Flowverses
A flowverse is a git repository with a flows/ directory in it. Put your flows in one and they are offered by name, as <flowverse>/<flow>, on every machine you add it to. It is how a weaver hands a flow to somebody else: reach for one to publish a flow somebody else can run, or to install one somebody else published.
Only installing one? Adding one is that half, and nothing before it is needed.
Try it
Publish two flows in a repository, add the repository, and run one by name.
- Lay out the repository. There is no manifest and nothing to register.
my-flowverse/
├── flows/
│ ├── review/ → yours/review
│ │ ├── __init__.py the flow
│ │ └── skills/ what it brings, mounted onto its agents' sessions
│ │ └── review-notes/SKILL.md
│ ├── nightly.py → yours/nightly, a flow that brings nothing
│ └── _shared.py → not a flow; imported by the two above
├── tests/ → not read: only flows/ is
└── README.mdreview is a directory whose __init__.py holds the function marked @flow:
# flows/review/__init__.py
"""Review the current diff and write the findings to REVIEW.md."""
from hmz.flows import Agent, flow
@flow
def run(agents: tuple[Agent], task: str) -> None:
(agent,) = agents
agent(f"Read the diff and write what is wrong to REVIEW.md.\n\n{task}", suppress=True)- Push it.
cd my-flowverse
git init -q && git add -A && git commit -qm "two flows"
git remote add origin git@github.com:you/my-flowverse.git
git push -u origin main- Add it. At the prompt, v in
/flow— or/flowverses— and then a: a URL or anowner/repo, andyoursas the name to keep it under. That clones the repository into~/.humanize/flowverses/yours/, and enter on it reads back what it holds by the name-ftakes. From a script the same two areHmz().verses:
from hmz.sdk import Hmz
verses = Hmz().verses
verses.add("you/my-flowverse", "yours")
verses.holds(verses.find("yours"))- Run one.
hmz exec -f yours/review -a claude/claude-opus-4-8:high "the payments module"yours/review is the qualified spelling, <flowverse>/<flow>, the one spelling nothing can stand in for. That is the whole feature: flows in a repository are offered by name, on every machine you add the repository to.
The one that is always there
official is humanize's own, and it is kept in two places: chat is in the package, because an interface that has never reached a network still has to have something to open talking to, and everything else is in humanfia/flowverse. Which of the two a flow is in is humanize's business rather than yours, so both are offered under the one name and every flow of humanize's is run by a bare one:
hmz exec -f rlar -a claude/claude-opus-5:max -a codex/gpt-5.6-sol:max "$(cat TASK.md)"official/rlar is the qualified spelling and still resolves — it is the one that pins a flow to the place it came from — but nothing needs it, and a flow that moves from the package into the flowverse goes on answering to the name it always had.
It cannot be taken away, and it is listed before it has been fetched, because what there is to run is not the same question as what has been downloaded. Opening /flow fetches whatever has never been fetched, so in practice it is there by the time you look; from then on every start of the interface takes what each repository says now, in the background and without a word about it — except for a clone you have written into, which is left where it is, a fetch being what would take the edit back. A flow from a flowverse that has not been fetched says so, rather than saying there is no such file. The name is right; the download has not happened.
Adding one
From a script
Hmz().verses is the same store, reached without opening anything:
from hmz.sdk import Hmz
verses = Hmz().verses
verses.all() # what places flows come from
verses.add("you/my-flowverse", "yours")
verses.holds(verses.find("yours")) # what it holds, by the name -f takes
verses.fetch("yours") # again, or for the first time
verses.remove("yours") # flows and allUse this for a machine being set up, a CI job that runs a flow somebody else wrote, or anywhere the interface is not open. What it added is findable by -f at once:
hmz exec -f yours/review -a claude/claude-opus-5:high "the payments module"addnames it after the repository when you do not, asgit clonedoes.allanswers with the places themselves, in the order their flows are offered, so a script reads the names off the objects rather than off anything printed.holdsanswershumanize1:gen-plan, not thehumanize1its filename would suggest. Working that out means importing the files, which is what/flowdoes for the same question.all,addandfetchread nothing, so a repository you have just cloned is never run until you ask what is in it.
See SDK reference.
At the prompt
v in /flow opens the places themselves, and /flowverses opens the same menu:

| Key | |
|---|---|
| enter | What that flowverse holds — which means importing its flows, so it is asked of the one you open rather than of the whole list. Past the flows, the row that takes the flowverse away |
| a | Add one: a URL or an owner/repo, and a name to keep it under — blank for the repository's own |
| r | Fetch the one under the cursor again, or for the first time |
What a flowverse holds is mostly something you read rather than choose from: each flow's name, and the line it says about itself. The one row that is not a reading is the last, which takes the whole place away, flows and all. It is in here rather than on a key of the list because what a flowverse is, is what is in it — so being rid of one is decided where that has just been read. builtin, official, local and user do not offer it, and say why where it would have been.

One that has never been fetched has nothing to read yet. It says so, and says that r fetches it, rather than reading as a place with nothing in it. A fetch runs off the interface's own loop: it keeps drawing while it clones, and what became of it is said under the list rather than thrown at you. Opening /flow fetches whatever has never been fetched, in the background and without moving what you are reading, so r is for fetching one again.
Which flow to run is /flow's question, and it steps between the same places:
| Key | |
|---|---|
| ← → | Step between the places flows come from, a list apiece — every flowverse, ending with local and user where either holds anything |
| f | Copy the flow under the cursor into .humanize/flows/, to change |
| v | The places themselves, which is this menu; esc comes back to the flows |
A flow whose file will not import is still listed there, under the name it would have had and with nothing beside it. It is a flow somebody named. Saying so where it is chosen beats leaving it off the list and letting you wonder where it went.
The two are apart because they are two questions: which flow to run, and what places there are. Walked to rather than turned to, and a sheet of its own rather than a deeper view of the flow menu: that menu holds everything until you save it, and adding a place, fetching one and taking one away each run git the moment you ask. /flowverses stays a command as well, since the key belongs to the flows and there are none to choose from while a flow is running. What stays on the flows is f, which is about the flow you are looking at — the moment you find out it is nearly what you want. A flow is a directory, so the copy is the whole of it, skills and all, and it lands under the name it already had. Yours are looked in first, so from then on that name means your copy. Editing a flowverse's own copy would not keep, since fetching it again takes what that repository says now.
Typing /flow and pressing enter still sends /flow. A command that has been written out whole is offered nothing to finish it with, so /flowverses never sits under a cursor waiting to be taken by the enter that meant to send the shorter one.
Your own flows are a place too
The flows in .humanize/flows here and in ~/.humanize/flows are two more places flows come from, called local and user. They are flowverses in everything but the fetching: read where they lie, listed beside the rest, and offered under the name of the place they are in. Nothing clones them and nothing can take them away, so add, fetch and remove all say so.
One list, so one rule for what a flow is called and one place a name is looked up.
Where a name is looked for
-f takes a name or a path. A name is looked for nearest first:
local | .humanize/flows/* — this project's own |
user | ~/.humanize/flows/* — yours, in every project |
| — | humanize's own, and every flowverse there is |
Nearest wins. A flow of your own may stand in for one of humanize's by taking its name: a .humanize/flows/chat/__init__.py is what -f chat runs in that project.
What a flow is called is a separate question, and every place answers it the same way:
chat · rlar | humanize's own, wherever of its two places each is kept |
yours/review | one somebody else's flowverse holds — the one spelling nothing can stand in for |
local/chat | this project's own |
user/chat | yours, in every project |
Yours is listed beside humanize's rather than instead of it, and what each was set up to run is remembered apart.
A name that no place answers to is taken as a path — a flow being written, a file a script wrote out. Both shapes work: flows/nightly finds flows/nightly/__init__.py and flows/nightly.py alike. A file whose name starts with _ is not a flow.
A flowverse is a library too
load takes exactly what -f takes, so a flow in a flowverse can be called from inside another flow:
from hmz.flows import load
plan = load("humanize1:gen-plan")
plan(agents, f"plan this first: {task}")A name nothing answers to is refused where you ask for it, rather than an hour into your loop. Publish two small flows rather than one large one for exactly this reason.
Making one
Any git repository will do, laid out the way Try it lays one out, and held to six rules:
| Rule | |
|---|---|
the flows go in flows/ | and nothing outside it is read, or run |
| one directory per flow | its __init__.py holds the function marked @flow |
or a single .py | for a flow with nothing to bring and nothing to import |
a name starting with _ is not a flow | which is where shared code goes |
| the flow's docstring's first line | is what is shown beside its name |
| one file may hold several | @flow(name="…"), run as <flow>:<name> |
Add it with a in /flowverses, or clone it into ~/.humanize/flowverses/<name>/ yourself.
Whoever adds your flowverse is trusting it with their machine. Earn it. Say in the README:
- What each flow drives: how many agents, and what each is for.
- Which backends it needs. A flow that hangs a
PERMISSION_REQUESThook needs Claude Code or Codex; one built onpursueneeds a backend with a goal feature. - What it writes. Files, branches, commits, pushes.
- The
hmz execline that starts it, verbatim. Each of humanize's own flows names its own in its docstring; do the same.
Adding one is trusting that repository with this machine
A flow is Python, and reading a flow means running it. Listing what a flowverse holds imports every file in its flows/. Add the ones you would clone and run, exactly as you would install a package.
So: no side effects at import time, nothing that reaches the network as the module loads, and no _shared.py that does anything on import beyond defining things. Whatever a flow does as it is imported is the flow's own business, and it fails for somebody who was only browsing the list. See Security.
Check it in the repository's own CI before anybody else does. A flow that stopped loading is a red build:
from hmz.flows import drives, wanted
drives("yours/review") # loads it exactly as `-f` would
wanted("yours/review") # what somebody choosing the agents will be askedSee also
- Flows — every flow the two of them hold, with the shape of each drawn
- Flows › Flowverses
- The official flowverse
- Testing a flow