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:


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:


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:

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