"I know I ran this exact
nmapcommand three days ago. I just can't remember the flags, the target, or which one of my 4,000 shell history lines it's hiding in."
wf is the fix: a terminal UI for saving, organizing, and running
parameterized shell commands — think Warp's Workflows feature, minus the
rest of Warp. Workflows are plain YAML files, sorted into folders,
browsable with a keyboard or a mouse, and every run gets logged so
"did that actually work?" is a keystroke away instead of a guess.
Built on Bubble Tea, Lip Gloss, Bubbles, and Huh — all the good Charm stuff.
┌─ Workflows ────────────────────────────────────────────┐
│ 📁 infra │
│ 📁 backups │
│ ⚡ deploy prod ✓ 2h ago · the scary one │
│ nmap discovery scan ✗ 6d ago · finally, an audit │
│ update os │
└────────────────────────────────────────────────────────┘
↑/↓ navigate n new N new folder enter run/open
h history m move d delete q quit
Save a command once. Give its {{placeholders}} names. Never type it
from memory again — and never wonder whether last Tuesday's run actually
succeeded, because it's sitting right there in history, exit code and all.
go install github.com/itheCreator1/wf/cmd/wf@latestThat drops a wf binary in $(go env GOPATH)/bin — add it to your PATH
if it isn't already, and wf is a command like any other. Working from a
clone instead:
go run ./cmd/wfWorkflows live in $HOME/.config/wf/workflows — one fixed location no
matter which directory you launch wf from, so "wait, which set of
workflows am I looking at" is never a question you have to ask. Override
with -dir or WF_WORKFLOWS_DIR when you mean it (fixtures, testing,
a second brain):
go run ./cmd/wf -dir ~/my-workflows
# or
WF_WORKFLOWS_DIR=~/my-workflows go run ./cmd/wfOr build it once and forget go run exists:
go build -o wf ./cmd/wf
./wfPlatforms. Linux and macOS are the supported targets — both are built
and tested in CI. wf compiles and runs on Windows too, with one caveat:
there's no PTY layer there, so an interactive: true workflow still runs
correctly against the real terminal and still reports its exit code, but
its output transcript comes back empty rather than being recorded to
history.
Folders first, workflows after, both alphabetical. Everything below works from the keyboard and the mouse — click a row to select it, click an already-selected one to open it, or just grab a workflow and drag it onto a folder to relocate it.
| Key | Does what it says |
|---|---|
↑/↓ (j/k) |
navigate — or just click |
/ |
fuzzy filter |
enter / r |
run the workflow, or step into the folder |
backspace |
step back out |
n |
new workflow (lands wherever you're currently standing) |
b |
new notebook |
N |
new folder |
e |
edit the selected workflow or notebook |
m |
move it to another folder (or just drag it) — works for workflows and notebooks alike |
h |
its run history — workflows only; a notebook has no single history of its own, so this shows a note pointing you at the workflow(s) it runs instead |
d |
delete — folders must be empty, workflows and notebooks both ask nicely first |
q / ctrl+c |
quit |
| Key | Does what it says |
|---|---|
tab / shift+tab |
hop between fields |
ctrl+s |
save |
esc |
cancel (twice, if you'd actually lose something) |
ctrl+a |
add an argument row — auto-fills its name if your command already has an undeclared {{placeholder}} sitting there, unclaimed |
ctrl+x |
remove the focused argument row |
ctrl+↑ / ctrl+↓ |
reorder it |
Type a {{placeholder}} you haven't declared and it gets flagged
immediately, right under the command field — no surprise at run time,
no "why didn't this substitute."
Got arguments? A tiny form asks for them first, defaults pre-filled,
enter to go. No arguments? Straight to execution.
Output streams live, stderr picked out in red, ctrl+c aborts mid-flight,
esc/q closes once it's done. Mark a workflow interactive (⚡ in the
list) and wf hands your actual terminal over instead — sudo prompts,
ssh, pagers, and editors all behave exactly like they would if you'd
typed the command yourself, because for a moment, you basically did.
Either way, the run gets recorded — command, exit code, timing, and the output itself, PTY sessions included (captured through a real pseudo-terminal; if that ever fails to allocate, the workflow still runs, just without the transcript — a graceful shrug, not a dead end).
Press h on a workflow to see every run it's ever had, freshest first:
status, timestamp, how long it took, the exact command that fired, and
what it printed — collapsed to a few lines by default, o to unfold the
whole thing. The list itself shows a quick ✓ 2m ago / ✗ 6d ago badge
per workflow, so you don't even have to ask. The last 200 runs stick
around; after that, the oldest quietly retire.
Press h on a notebook and you'll get a note instead of a history list —
deliberately: a notebook has no single history identity, since it can
reference zero or many different workflows via {workflow=slug} blocks
(each recorded under that workflow's own slug), and ad-hoc {run}
blocks are never recorded at all. Check the referenced workflow's own
history instead.
Real subdirectories on disk. No sidecar metadata file to fall out of sync,
no migration if you've never made one — an install with zero subfolders
just is the root folder, and always was. N makes one, enter walks
into it, m (or a drag) moves a workflow or notebook in, d clears it
out once it's empty.
A workflow is one command. A notebook (📓 in the list, .md file
instead of .yaml) is a runbook: prose explaining what you're doing,
interleaved with the actual commands to do it, each one individually
runnable right where it sits.
📓 Deploy Runbook
# Deploy Runbook
First, check current status:
╭──────────────────────╮
│ run │
│ echo checking status │
╰──────────────────────╯
Then deploy:
╭───────────────────────────╮
│ workflow: Deploy Prod │
│ echo deploying to {{env}} │
╰───────────────────────────╯
↓/tab next block enter run p preview esc/q back
A fenced code block becomes runnable by adding an attribute right after
its language, inside { }:
```sh {run}
echo just run this literally
```
```sh {run interactive}
sudo systemctl restart nginx
```
```sh {workflow=deploy-prod}
```{run}— runs the block's own body.{{placeholder}}tokens work here too, same argument-fill form as a workflow with arguments.{run interactive}— same, but hands the real terminal over (sudo,ssh, pagers, editors), exactly like a workflow'sinteractive: true.{workflow=<slug>}— runs an existing saved workflow by its slug instead. The fence body is left empty on purpose: the real command lives in that workflow's own file, so it can't drift out of sync with what actually runs. This is the only kind of block whose run gets recorded to history, same as running it from the list — an ad-hoc{run}block doesn't, on the theory that a notebook is a scratchpad of commands, not itself a saved, named thing worth a history entry.- Any other fenced block — no attribute at all — is just an example for reading, never runnable. The attribute is required; a bare command typed as plain paragraph text (no fence) is treated as prose too, with no visual cue that it's inert. If you paste a command in and it isn't doing anything, this is almost always why.
↑/↓ (or j/k/tab/shift+tab) jump between runnable blocks only —
prose is for reading, not stopping on. enter runs whichever one's
focused. p toggles a fully-rendered markdown preview of the whole
document (via Glamour) if you
just want to read it. e opens the raw markdown source in wf's own
built-in editor — no $EDITOR handover, just a plain text box — to write
or restructure the whole thing; alt+p there splits the editor
side-by-side with a live-updating rendered-markdown pane, so you can keep
typing on the left and watch the right pane update as you go (unlike
browse mode's p, this doesn't replace the editor or swallow keystrokes —
alt+p again closes the split back to a full-width editor), ctrl+s
saves.
Since it's just markdown with an attribute in a code-fence's info string — not a comment, not a custom syntax — a notebook renders perfectly normally on GitHub, in VS Code, anywhere: the attribute is invisible noise to every other tool, and the fence still gets full syntax highlighting since its language tag is untouched.
One YAML file per workflow, named after a slug of its name, living
wherever you filed it:
name: Docker Tail Logs
description: Tail logs for a running container
command: docker logs -f --tail {{lines}} {{container}}
arguments:
- name: container
description: Name or ID of the container
default: ""
- name: lines
description: Number of lines to show before tailing
default: "100"
tags:
- docker
interactive: falsenameandcommandare the only required fields.arguments— each one declares a{{name}}your command can reference, with an optionaldescriptionanddefault.tags— free-form, currently just fodder for the fuzzy filter.interactive— flip this on for anything that needs to talk to the terminal:sudo,ssh, pagers, editors, confirmation prompts.
Commands run via $SHELL -c '<command>' (or /bin/sh if $SHELL is a
mystery), so pipelines and builtins work fine — but -c doesn't source
your rc-file aliases and functions, interactive or not. That flag's job
is stdin: a normal run gets /dev/null (fine, until it isn't), an
interactive one gets the real terminal, prompts and all.
History lives separately, at ~/.config/wf/history.jsonl, right next to
whichever workflows directory is currently in play.
The twist — yes, you can make wf read its own documentation
Since every shell command is fair game, nothing's stopping you from
onboarding yourself with wf itself. Drop this in your workflows
directory:
name: how do I use this thing
description: read the manual, but make it an interactive workflow
command: less README.md
interactive: trueSave it, run it, and wf will suspend itself, hand you a pager, and let
you read exactly this sentence from inside the tool it's describing. It's
turtles, but they're helpful turtles.
cmd/wf/ entrypoint — flags, env, store/history init, bootstrap
internal/workflow/ the domain model: validation, placeholders, no I/O
internal/notebook/ the notebook domain model: markdown parsing, block/
attribute grammar, no I/O
internal/store/ YAML + Markdown persistence — load/save/delete, folders,
atomic writes, for both workflows and notebooks
internal/history/ the run log — JSON lines, retention, per-workflow lookup
internal/runner/ non-interactive execution — streaming output, cancellation
internal/ptyrun/ interactive execution via a real PTY, with output capture
and a graceful fallback if a PTY can't be had
internal/ui/ the Bubble Tea app — state machine, mouse handling,
history/folder views; list/, edit/, run/, notebook/
hold the rest
workflows/ seed workflows for development — not your real ones
go build ./...
go vet ./...
go test ./...
gofmt -l .CI runs exactly these four on every push and pull request, across Linux and macOS — see .github/workflows/ci.yml.
internal/workflow, internal/notebook, internal/store, internal/history,
internal/runner, and internal/ptyrun are unit tested. The TUI itself,
including internal/ui/run and internal/ui/notebook, is verified the
old-fashioned way — actually running it — since Bubble Tea rendering logic
and unit tests don't have much to say to each other.
Now go save that nmap command before you have to reverse-engineer it
from your history again.