Humanize in CI
Run a flow from a scheduled job, open a pull request with what it changed, and keep a trace of what each agent did. By the end of this page you have a nightly GitHub Actions workflow that does all three, and know what each step is for so you can move it to another CI. Reach for it when a loop should work on your repository while nobody is at a terminal.
Try it
Commit a TASK.md saying what the loop should work on, add a repository secret your agent's CLI signs in with, and run the same line CI will run, on your own machine first:
hmz exec \
-f ralph_loop \
-a agent=claude/claude-opus-5:high \
-p budget.duration=45m,budget.cost=20 \
"$(cat TASK.md)"When that does what you want, put it in the workflow below. The line is explained in Run it unattended; this page is the job around it.
The agents run unsupervised, with the job's access
A flow's agents run with approvals bypassed, and on a runner they can reach whatever the job can: its token, its secrets, the network. Give the job only the permissions and secrets it needs. See Permissions.
Before you start
- A line that works by hand. Everything CI does wrong, it does slowly. Try the
hmz execline at a terminal first: see Run it unattended. - A way to sign the agent's CLI in without a browser, stored as a repository secret: a token from
claude setup-tokenfor Claude Code, an API key for Codex. See Sign the agent's CLI in. - Permission for the job to push a branch and open a pull request:
contents: writeandpull-requests: write, and, on GitHub, Allow GitHub Actions to create and approve pull requests switched on in the repository's settings.
What a runner is missing
A CI runner starts empty every time. That shapes the whole job:
| At your desk | On a fresh runner | So the job… |
|---|---|---|
hmz and the agent's CLI are installed | nothing is | installs both, every run |
| the CLI is signed in | it is not | signs it in from a secret |
| the flows you installed are there | only the built-in ones are | runs a flow Humanize ships, or names any other by its repository |
hmz keeps a price list for cost | there is none | fetches it before the first turn |
/epics reads a run back | there is no prompt | traces the run from a script |
| a stopped run can be picked up | nothing of a last run is kept | never uses --resume |
hmz exec does not open on what the interface was set up with in any directory. It uses what the machine holds: the flows installed on it, its accounts, its fallbacks, any CLI added on the Accounts page of /settings, and whether reporting was answered yes. A fresh runner holds none of these, so on a runner the line is the whole setup; a run is profiled only where the line says --profile-flow or --profile-environment.
Example: a nightly loop that opens a pull request
Commit this as .github/workflows/nightly.yml, with TASK.md and the ci/trace.py script beside it:
name: nightly
on:
schedule:
- cron: "0 2 * * *" # ①
workflow_dispatch:
permissions: # ②
contents: write
pull-requests: write
jobs:
loop:
runs-on: ubuntu-latest
timeout-minutes: 60 # ③
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v10.0.1
# For Kimi Code or DeepSeek Harness, install 'hmz[all]' instead.
- name: Install the agent's CLI and Humanize # ④
run: |
npm install -g @anthropic-ai/claude-code
uv venv --python 3.12 "$RUNNER_TEMP/hmz"
uv pip install --python "$RUNNER_TEMP/hmz/bin/python" \
hmz
echo "$RUNNER_TEMP/hmz/bin" >> "$GITHUB_PATH"
- name: Run the loop # ⑤
env:
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
run: |
hmz exec \
-f ralph_loop \
-a agent=claude/claude-opus-5:high \
-p budget.duration=45m,budget.cost=20 \
"$(cat TASK.md)"
- name: Trace the run # ⑥
if: always()
continue-on-error: true
run: python ci/trace.py "$RUNNER_TEMP/trace.json"
- uses: actions/upload-artifact@v5 # ⑦
if: always()
continue-on-error: true
with:
name: trace
path: ${{ runner.temp }}/trace.json
- uses: peter-evans/create-pull-request@v7 # ⑧
with:
branch: nightly/${{ github.run_id }}
title: "nightly: what the loop did"What each part means
cron: "0 2 * * *"runs it at 02:00 UTC every night.workflow_dispatchadds a Run workflow button, which is how you try it the first time.permissionsgives the job's token exactly what the last step needs: to push a branch and open a pull request. The agents can reach this token too, so give it nothing more.timeout-minutes: 60is GitHub's limit, not Humanize's. Keep it well above the run'sduration: the installs count against it, and the turn under way when the budget runs out is let finish unless the budget saysgraceful=false.- Install the agent's CLI and Humanize. The CLI from npm, and Humanize into an environment of its own whose
binis put on thePATH. Kimi Code and DeepSeek Harness also need thehmz[all]extra: see Installation. - Run the loop. The same
hmz execline you ran by hand, with the CLI signed in from a secret.-p budget.duration=45m,budget.cost=20stops it at 45 minutes or $20, whichever comes first, and the step still exits 0: a Ralph loop usually ends this way. A fresh runner has no price list, so a run with acostlimit fetches one before its first turn (at most 20 s). Thedurationstill bounds the run if the model has no price. - Trace the run.
if: always()traces a run that failed too, which is the one you most want to read.continue-on-errorkeeps a failed trace from failing the job. - Upload the trace as an artifact named
trace, kept with the job. - Open the pull request with whatever the loop changed. It runs only when the loop succeeded, so a failed run opens nothing.
Check that it worked
Run it once from the Actions tab with Run workflow, and read the log before you leave it to the schedule. The Run the loop step shows the run as it happens, in plain lines since a job log is not a terminal. Here is the same line run with a small model on a one-bug project, output redirected the way a job's is, shortened:
● agent is working
● Read(/home/runner/work/app/app/calc.py)
● Edit(/home/runner/work/app/app/calc.py)
● Bash(cd /home/runner/work/app/app && python -m pytest test_calc.py -v)
● Done. Changed `a - b` to `a + b` in the `add()` function, and the test passes.
✻ input 42 · output 760 · cache_read 99.0k · cache_write 8.3k · claude-haiku-4-5-20251001 · agent
✻ Worked for 10s · agent
…
hmz exec: stopped -- ralph_loop:ralph_loop: its budget's duration is spent● …lines are what the agent did,✻lines close each turn with what it spent.hmz exec: stopped -- … duration is spentis the budget ending the run, with the step green.- The Trace the run step prints how the run ended,
the run ended: stopped, and thetraceartifact is on the run's summary page. - A pull request named
nightly: what the loop didis open, on a branch named after the workflow run,nightly/<run id>.
A run that went wrong before any agent started fails the step in seconds with exit status 2 and hmz exec: error: … naming why.
Sign the agent's CLI in
Humanize holds no credentials. An agent with no @account runs its CLI as the runner has signed it in, so sign it in the way that CLI supports without a browser:
# a token from `claude setup-token`, stored as a repository secret
- name: Run the loop
env:
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
run: hmz exec -f … -a agent=claude/claude-opus-5:high …# install @openai/codex rather than claude-code, then hand Codex's own login an API key
- name: Sign Codex in
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: printenv OPENAI_API_KEY | codex login --with-api-key
- name: Run the loop
run: hmz exec -f … -a agent=codex/gpt-5.6-sol:high …To run agents as named accounts on the runner instead, make them from Python with Hmz().accounts before the run. See the SDK reference.
A flow from a flowverse
ralph_loop ships with Humanize, as do chat and the other loops FlowBench scores, so -f ralph_loop runs on a fresh runner as it is. A flow you install from a flowverse is not there: a fresh runner has fetched no flowverse and installed nothing, so -f flame_chasoid:parallel_git_pr is refused with the official flowverse has not been fetched yet. Name that release by its repository instead, which fetches it for the run and installs nothing:
-f 'git+https://github.com/humanfia/flame-chasoid-flows@v0.1.0#flame_chasoid:parallel_git_pr'The @v0.1.0 is a tag; a commit pins the flow so that nothing upstream can change what runs at night. A flow of your own needs none of this: commit it to .hmz/flows/ and name it with -f <name>.
Keep a trace
At a terminal you get a trace from /epics. A runner has no prompt, so the workflow calls the same thing from Python, with the Python Humanize was installed with:
# ci/trace.py
"""Trace the run that just happened, and say how it ended."""
import sys
from hmz.sdk import Hmz
epics = Hmz().epics
if runs := epics.all(): # every run in this directory, oldest first
run = runs[-1]
epics.traced(run, output=sys.argv[1])
ran = epics.read(run)
print("the run ended:", ran.how if ran and ran.how else "unfinished")Run after a loop its budget stopped, it prints:
$ python ci/trace.py "$RUNNER_TEMP/trace.json"
the run ended: stoppedhow is done, failed or stopped. The trace is written outside the checkout so the pull request does not pick it up. Download the trace artifact and drop it into ui.perfetto.dev: a process per agent, a slice per thing it did. See Tracing a run.
Variations
Read the run with a program
For a step that reads the run rather than shows it, add --json. shell: bash adds pipefail, so the step fails when hmz exec does:
- name: Run the loop
shell: bash
run: |
hmz exec … --json "$(cat TASK.md)" \
| tee "$RUNNER_TEMP/run.ndjson" \
| jq -r 'select(.kind == "tool") | .text'The job log then holds one line per tool call:
Read /home/runner/work/app/app/calc.py
Read /home/runner/work/app/app/test_calc.py
Edit /home/runner/work/app/app/calc.py
Bash cd /home/runner/work/app/app && python -m pytest test_calc.py -v# output tokens across the whole run
jq -s 'map(.spent.output // 0) | add' "$RUNNER_TEMP/run.ndjson"The objects are described in Run it unattended › Read it with a program.
Colour in the job log
A job log is not a terminal, so the lines come out plain. Set FORCE_COLOR: "1" on the step for colour a log viewer renders.
Keep the line in the repository
Put the hmz exec line in a script, such as ci/nightly.sh, and call that from the workflow. Then you can run exactly what CI runs on your own machine, and a change to the line is reviewed like any other change.
Another CI
Nothing but the workflow file is GitHub's. On any CI that runs a shell, the job is the same five steps: install, sign in, hmz exec, python ci/trace.py, and keep the trace.
Act on the exit status
| Status | Means |
|---|---|
0 | The flow returned, or its budget stopped it. |
1 | The run failed: the flow raised an error it did not handle. |
2 | Refused before any agent started: an -a it can't read, a role left unfilled, no budget, a flow that isn't there. |
| anything else | The job was cancelled: 143 for a terminate signal, 130 for an interrupt, once the run has let go of what it started. |
A 2 fails the job in seconds rather than after forty minutes.
Pitfalls
- The first turn fails, and the step goes red with status 1. The CLI is not signed in. Nothing catches that before the run, and a turn that fails ends the run with what the CLI said. Read the log of the first run by hand.
nobody lists a price for …, so cost=… cannot stop what it spends. The model is not on the price list, or the runner could not fetch the list (no network, orHUMANIZE_PRICES=off), so thecostlimit never fills. Keep adurationoroutput_tokenslimit beside it, as the workflow does.--resumeis refused with status 2. The runs a flow picks up from are kept on the machine that ran them, and a runner starts empty every night. Leave--resumeoff in CI.- The job is killed before the loop ends.
timeout-minutesis below the run'sdurationplus the installs. Raise it. - No pull request appears. The loop failed, so the step was skipped; or the repository does not allow GitHub Actions to create pull requests.
More in Troubleshooting.
Next steps
- Run it unattended: every part of the
hmz execline - Tracing a run: reading the trace the job keeps
- Accounts: running an agent as a named account
- Permissions: what the agents may touch on the runner
- CLI reference and SDK reference