Write harnsy actions here: scripts an agent proposes and you run from the harnsy dashboard with one button, with a status, a summary and figures for every run.
harnsy runs an action in its project's directory. Press Use this
template and either make your copy the project, or copy actions/,
cmd/, internal/, Makefile and the agent files into the project the
actions are for. Then start from one of the three examples.
| Example | Kind | What it shows |
|---|---|---|
actions/disk |
bash script | the smallest action: a parameter, ::step, ::fail, a result with figures |
actions/todo-count |
Go, one file | a Go script harnsy runs with go run: standard library only |
actions/go-stats |
command (Go binary) | a program built from this module (cmd/ + internal/), with tests, using the contract helper internal/action |
make check # gofmt, go vet, tests, build, every action run once
make try A=disk # run an action the way harnsy runs it
make try A=disk P='-p max_used=95'
go run ./cmd/propose -dry-run actions/disk # the proposal, printed
go run ./cmd/propose -from <agent> -project <project> actions/diskmake try gives the action what harnsy gives it (the working directory,
a trimmed environment, ACTION_RESULT, ACTION_DIR, the time limit),
prints its output and its last ::step and ::fail, and checks the
result against the contract. A proposal waits on the dashboard until you
enable it; nothing runs before that.
Each action is a directory in actions/:
actions/<name>/
action.json the definition (contract/action-definition.schema.json), without script.body
run.sh the script, by lang: run.sh (bash, sh), run.py, run.js, main.go
cmd/propose puts the script into script.body when it sends the
proposal. A command action has no script file: its command.argv[0] is a
file in the project.
{
"name": "disk",
"title": "Disk",
"script": {"lang": "bash"},
"params": [{"name": "max_used", "type": "int", "min": 1, "max": 100, "default": 90}],
"timeout": "1m",
"numbers": [{"key": "free_gb", "label": "Free, GB"}, {"key": "used_gb", "of": "size_gb"}]
}The full text is contract/contract.md (version
1.1.0, as harnsy serves it at /docs/actions/contract.md), with JSON
Schemas and an OpenAPI description next to it.
- Arguments. One per parameter, in the declared order (
$1,$2, …); in a command, an argument that is exactly{name}becomes that value (--level={level}stays as written: use--level{level}). Never a shell string. Values are trimmed:enumthe value,intanditemdigits,booltrueorfalse(1,t,TRUEare accepted),committhe full 40-character hash a branch, tag or commit resolves to in the action's directory. - Where. On a Linux node, for now.
- Environment. Only
PATH,HOME,LANG,USER,TMPDIR,HARNSY_HOME(so the harnsy CLI reaches this node), the names the action lists inenv, andACTION_CONTRACT(1),ACTION_RUN_ID,ACTION_RESULT(write the result there),ACTION_DIR(a directory of the run's own). - Markers. A whole line of output (standard output or error):
::step <text>(what runs now) and::fail <text>(why it failed, one line). - Exit. 0 is ok, anything else failed; past
timeout(1m to 2h, default 30m) the run is stopped. When the script exits, what it left running is stopped too. - Result. JSON in
$ACTION_RESULT, up to 64 KiB, every field optional:
{
"summary": "5.1 GB free of 456 GB (99% used)",
"numbers": {"free_gb": 5.1, "used_pct": 99},
"artifacts": [{"path": "dist/app.tar.gz", "label": "build"}],
"links": [{"url": "https://example.com/report/7", "label": "report"}]
}At most 8 numbers (a number or a text of up to 40 characters), 20
artifacts inside the project or ACTION_DIR, 10 http(s) links.
You enable one version of an action; harnsy pins it by sha256 and every change is a new version that waits for you again.
- A script (bash, sh, python, node, go): the whole body is pinned and shown to you before you enable it.
- A command: harnsy pins the file
argv[0]only, not what it calls. Propose a built binary (bin/go-stats): a build of changed code has another sha256, so harnsy turns the action off and waits for you to enable it again. Do not propose a wrapper such asrun.shthat callsgo run ./cmd/x: the Go code under it would change without asking you.
- One file (
lang: go): harnsy runsgo run <copy>.gowith the action's directory as the working directory, so the project'sgo.modapplies: the script could import the project's packages, and atoolchainline may download a Go toolchain. Keep it to the standard library: harnsy pins the script, not what it imports (make tryfails on any other import). Copy the few helper lines fromactions/todo-count/main.go. - A program (command):
cmd/<name>/main.gowith its logic ininternal/<name>and tests next to it, the contract throughinternal/action(FromEnv,Step,Fail,Result.Check,Env.Write).make buildputs it inbin/.
make check is what CI runs (.github/workflows/ci.yml), plus
golangci-lint (.golangci.yml) and
shellcheck on the bash scripts.
internal/def tests that every action in actions/ loads, passes the
checks harnsy makes and runs with its defaults.
AGENTS.md is the guide for coding agents in this
repository. Skills to write, test and propose an action are in
.agents/skills/ (Codex, OpenCode) and .claude/skills/ (Claude Code).
The contract has a semantic version. ACTION_CONTRACT holds its major
number; a script of major 1 keeps working on every 1.x harnsy. When the
contract adds something (a minor version), update contract/ from your
node's /docs/actions/ files.
MIT.