Skip to content

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.

  1. 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.md

review is a directory whose __init__.py holds the function marked @flow:

python
# 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)
  1. Push it.
sh
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
  1. Add it. At the prompt, v in /flow — or /flowverses — and then a: a URL or an owner/repo, and yours as 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 -f takes. From a script the same two are Hmz().verses:
python
from hmz.sdk import Hmz

verses = Hmz().verses
verses.add("you/my-flowverse", "yours")
verses.holds(verses.find("yours"))
  1. Run one.
sh
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:

sh
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:

python
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 all

Use 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:

sh
hmz exec -f yours/review -a claude/claude-opus-5:high "the payments module"
  • add names it after the repository when you do not, as git clone does.
  • all answers 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.
  • holds answers humanize1:gen-plan, not the humanize1 its filename would suggest. Working that out means importing the files, which is what /flow does for the same question.
  • all, add and fetch read 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:

/flowverses: the places flows come from, and what one of them holds

Key
enterWhat 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
aAdd one: a URL or an owner/repo, and a name to keep it under — blank for the repository's own
rFetch 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.

what a flowverse holds: a flow apiece, each with the line its own flow says about
itself

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
fCopy the flow under the cursor into .humanize/flows/, to change
vThe 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 · rlarhumanize's own, wherever of its two places each is kept
yours/reviewone somebody else's flowverse holds — the one spelling nothing can stand in for
local/chatthis project's own
user/chatyours, 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:

python
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 flowits __init__.py holds the function marked @flow
or a single .pyfor a flow with nothing to bring and nothing to import
a name starting with _ is not a flowwhich is where shared code goes
the flow's docstring's first lineis 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_REQUEST hook needs Claude Code or Codex; one built on pursue needs a backend with a goal feature.
  • What it writes. Files, branches, commits, pushes.
  • The hmz exec line 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:

python
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 asked

See also

Released under the Apache-2.0 licence.