One-shot hooks

Hooks always run in batches. Each script process sees batch env vars:

Variable Single-batch run
BATCH_INDEX 0
BATCH_COUNT 1
BATCH_ALLOCATIONS Comma-separated IPs in that sole batch (one or many workers)

A one-shot run means the hook completes in one batch — no second wave with BATCH_INDEX=1. Use this for operator maintenance, global migrations, or scripts that must see every allocation at once.

See also: hooks.md · hook-api.md


One worker

When the job has one active allocation, every cli / deploy event naturally produces BATCH_COUNT=1 and BATCH_INDEX=0.

maand hooks hook_status api --verbose

To target one worker while others stay active, register the hook for job_control (not only cli) and pass --allocations:

maand job run api --target migrate --allocations 10.0.0.2

Maand runs the job_control script on that worker only. Env includes TARGET=migrate, BATCH_COUNT=1, and BATCH_ALLOCATIONS=10.0.0.2.

maand hooks has no worker filter — it always includes all active allocations for the job.


All workers in one batch

Set max_concurrent_upgrades in manifest.json to at least the number of active allocations, then rebuild:

{
  "selectors": ["worker"],
  "max_concurrent_upgrades": 8,
  "hooks": {
    "hook_cluster_status": {
      "executed_on": ["cli"]
    }
  }
}
maand build
maand hooks hook_cluster_status api --verbose

Every active worker runs in parallel inside batch 0. Scripts see BATCH_COUNT=1 and BATCH_ALLOCATIONS listing all IPs (order follows rollout_order).

Tune max_concurrent_upgrades back down before production deploys if you normally roll in smaller bursts — see rolling-deploy.md.


Guard in the script

Maand starts one hook process per allocation in each batch. Every process in the same batch wave sees the same BATCH_* env, but each has its own ALLOCATION_INDEX. A check like BATCH_COUNT == 1 alone still runs on every allocation — not once per maand invocation.

For logic that must run exactly once (leader election, global setup, one-time debug logging), guard with is_one_shot() / isOneShot() from embedded maand.py / maand.ts (equivalent to BATCH_INDEX == "0" && ALLOCATION_INDEX == "0"):

import { isOneShot } from "./maand";

if (isOneShot()) {
  console.log("BATCH_ALLOCATIONS=" + process.env.BATCH_ALLOCATIONS);
  console.log("BATCH_COUNT=" + process.env.BATCH_COUNT);
  console.log("BATCH_INDEX=" + process.env.BATCH_INDEX);
}
import maand

if maand.is_one_shot():
    import os
    print("BATCH_ALLOCATIONS=" + os.environ.get("BATCH_ALLOCATIONS", ""))
    print("BATCH_COUNT=" + os.environ.get("BATCH_COUNT", ""))
    print("BATCH_INDEX=" + os.environ.get("BATCH_INDEX", ""))

Env vars are always strings — compare to "0", not 0, when checking raw env.

Other helpers for per-allocation or per-wave logic:

def is_single_batch() -> bool:
    return int(os.environ.get("BATCH_COUNT", "1")) == 1

def is_first_batch() -> bool:
    return int(os.environ.get("BATCH_INDEX", "0")) == 0

# Per-allocation work on every worker when the invocation is one batch wave
if is_single_batch():
    update_local_state()

# Per-allocation work only in the first batch wave of a rolling deploy
if is_first_batch():
    prepare_canary()
Check Use when
BATCH_COUNT == 1 Entire invocation is a single batch wave (all workers parallel)
BATCH_INDEX == 0 Logic should run only in the first batch wave of a rolling deploy
is_one_shot() / isOneShot() Logic must run exactly once (leader / debug print / global setup)
ALLOCATION_IP in BATCH_ALLOCATIONS.split(",") Logic scoped to workers in the current batch

Events compared

How you invoke Event Worker scope One batch (INDEX=0, COUNT=1)
maand hooks … cli All active One worker, or max_concurrent_upgrades ≥ active count
maand job run … --allocations ip job_control Filtered actives One IP in --allocations, or batch size ≥ filtered count
maand deploy deploy hooks Varies by event Same rules; batch width from manifest

Deploy pre_deploy / post_deploy / post_build follow the same batch rules as cli. Override order for the run with put_rollout_order() in pre_deploy if needed — hook-api.md.


Checklist

  1. Add executed_on (cli for ad-hoc, or job_control for per-worker maand job run).
  2. Set max_concurrent_upgrades (and rebuild) when all workers must share BATCH_COUNT=1.
  3. Test: maand hooks hook_<name> <job> --verbose and confirm env in logs or script output.
  4. Restore manifest batch size before rolling production deploys.