Tutorial: Hooks
Hooks are Python or Bun scripts under workspace/jobs/<job>/_hooks/. They run on the maand CLI host (not on workers) once per non-removed allocation (includes disabled), with access to maand’s runtime HTTP API and KV store.
Use them for migrations, secret handling, config generation, and deploy hooks — not for long-running services (those use the job Makefile on workers).
Prerequisites: quickstart · Deep dive: hook-api.md
Step 1 — Declare a command
Edit workspace/jobs/api/manifest.json:
{
"version": "1.0.0",
"selectors": ["worker"],
"hooks": {
"hook_migrate": {
"executed_on": ["cli", "pre_deploy"]
},
"hook_health": {
"executed_on": ["health_check"]
}
}
}
Rules enforced at build:
- Command names must start with
hook_ executed_onmust use allowed events:post_build,pre_deploy,post_deploy,job_control,health_check,cli- A job with
executed_on:["health_check"]cannot also define manifesthealth_checkprobes (build fails) - Matching script must exist:
_hooks/hook_<name>.py(or.ts/.js)
Step 2 — Add the script
workspace/jobs/api/_hooks/hook_migrate.py:
#!/usr/bin/env python3
import maand
def main():
job = maand.job()
worker = maand.worker()
print(f"migrate api on {worker['worker_ip']} alloc={job['allocation_id']}")
# Example: read KV
job_name = maand.kv.get("maand/job/api", "job_name")
print(f"job name from KV: {job_name}")
if __name__ == "__main__":
main()
Maand injects maand.py (or maand.ts) into each allocation workspace under tmp/ during execution.
Optional: job-local venv — place _hooks/.venv/ and maand uses that Python instead of system python3.
For Bun, use hook_foo.ts and ensure bun is on the CLI host.
Step 3 — Build and run from CLI
maand build
maand hooks hook_migrate api --verbose
Batch width comes from max_concurrent_upgrades in the job manifest (same as deploy). Override worker order with put_rollout_order() in the script if needed.
Before running, maand checks:
python3(or venv) for.pycommandsbunfor.ts/.jscommands
Step 4 — Wire into deploy
With pre_deploy in executed_on, maand deploy runs the command automatically before staging that job’s wave:
maand deploy --jobs api
Other useful events:
| Event | When it runs |
|---|---|
post_build |
End of maand build (build fails if hook fails) |
pre_deploy |
Before rsync for that job |
post_deploy |
After successful rollout |
job_control |
Instead of default make start/stop/restart |
health_check |
maand health_check and after deploy restart |
cli |
maand hooks only |
Step 5 — Dependencies and version
If api depends on database, both jobs need explicit version fields:
{
"version": "2.0.0",
"hooks": {
"hook_migrate": {
"executed_on": ["pre_deploy"],
"demands": {
"job": "database",
"hook": "hook_schema",
"config": {
"min_version": "1.0.0"
}
}
}
}
}
maand build verifies demand job/hook exist, both sides declare version, and upstream version satisfies min_version / max_version. Build also sets deployment_seq so database deploys before api.
Full reference: manifest.md · deploy upgrade env: deploy.md
Step 6 — Health check command
maand health_check --jobs api --wait --verbose
Or trigger after manual restart:
maand job restart api --health_check
KV and secrets from commands
Commands read/write KV via the runtime API (maand.kv.get, maand.kv.put, …). Build populates namespaces such as:
maand/worker/<ip>— worker metadatamaand/job/<job>— job metadatamaand/job/<job>/worker/<ip>— per-allocation certs and vars
Template files (.tpl) rendered at deploy use the same data. See hook-api.md (runtime API) and KV persistence / templates.md.
Quick reference
maand build # validates commands + scripts
maand hooks <hook> [job] [--verbose] # event: cli; omit job to run on all matching jobs
maand deploy # pre/post_deploy, job_control
maand health_check --jobs <job>
maand cat hooks # list registered commands
Next: day-2-ops.md · concepts.md