Job manifest reference

Canonical schema for workspace/jobs/<name>/manifest.json. For concepts (worker, job, allocation), see start/concepts. For dependency ordering, see deployment-sequence.md.


Job layout

workspace/jobs/database/
├── manifest.json       # required
├── Makefile            # required unless deploy uses only job_control commands
├── Makefile.tpl        # alternative to Makefile (rendered at deploy)
├── vars.conf           # optional job config → vars/bucket/job/<job>
├── _hooks/           # optional: hook_<name>.py | .ts | .js
├── *.tpl               # optional Go templates (rendered at deploy)
└── _prometheus/        # optional metrics — see [guides/prometheus](/guides/prometheus)

Scaffold:

maand job create myservice --selectors worker

Inspect built catalog:

maand cat jobs              # includes deployment_seq
maand cat hooks

Top-level fields

Field Purpose
version Semver-like release id; see Version
selectors Worker labels required for placement (all must match). When omitted, the job name is used — see Placement selectors.
max_concurrent_upgrades Rolling restart batch size during deploy (default 1)
max_concurrent_starts Rolling start batch size on first deploy (0 = all at once)
min_allocations_count Minimum non-removed allocations required after placement (default 0 = no minimum)
restart_policy How deploy applies updated allocations after rsync: always, reload, or never (default always)
restart_globs With reload only — globs; matching changed paths trigger restart instead of reload
resources Memory, CPU, ports — resources-and-placement.md
hooks Named hooks (hook_*) — cli/hooks.md
health_check Built-in probes (tcp/http/ssh) and/or a health_check command (probes run first)
certs TLS definitions → KV per allocation — certs.md

Example:

{
  "version": "2.1.0",
  "selectors": ["worker"],
  "max_concurrent_upgrades": 2,
  "max_concurrent_starts": 1,
  "resources": {
    "memory": { "min": "256 mb", "max": "1 gb" },
    "cpu": { "min": "200 mhz", "max": "1000 mhz" },
    "ports": { "database_port": 5432, "database_http_port": {} }
  },
  "hooks": {
    "hook_schema": {
      "executed_on": ["post_build", "pre_deploy"],
      "demands": { "job": "", "hook": "", "config": {} }
    }
  }
}

Port {} assigns from the bucket.conf pool; an integer fixes the port in the manifest. Each port key must be prefixed with <job>_ (for example job databasedatabase_port, job echo_serverecho_server_http).


Memory and CPU (resources)

Use resources.memory and resources.cpu in the manifest to declare min and max bounds for the job. These limits travel with the job in git and define what reservations are allowed on any bucket.

"resources": {
  "memory": { "min": "256 mb", "max": "2 gb" },
  "cpu": { "min": "500 mhz", "max": "2000 mhz" }
}
Field Meaning
min Lower bound for reservations (omit or 0 = no lower bound)
max Upper bound for reservations (if min is set and max omitted, max defaults to min)

Build fails when min > max.

Actual reservation (not in the manifest)

The manifest does not set how much memory or CPU the job uses today. That comes from workspace/bucket.jobs.conf (or an environment-specific file — see below). Each job section sets the current reservation:

# workspace/bucket.jobs.prod.conf
[api]
memory = "1 gb"
cpu = "1500 mhz"

The value must satisfy manifest min ≤ reservation ≤ manifest max. Build stores it as current_memory_mb / current_cpu_mhz and sums it per worker for capacity checks against workers.json.

Choosing an environment

Set job_config_selector in maand.conf to pick which override file build reads:

job_config_selector File
"" workspace/bucket.jobs.conf
"prod" workspace/bucket.jobs.prod.conf
"staging" workspace/bucket.jobs.staging.conf

Use the same manifest bounds in every environment; tune memory / cpu per file without changing manifest.json.

KV keys maand/job/<job> expose min_memory_mb, max_memory_mb, memory, min_cpu_mhz, max_cpu_mhz, and cpu for templates and commands.

Details: resources-and-placement.md · configuration.md.


Placement selectors

If selectors is set in the manifest, only those labels are used for placement. If selectors is omitted or empty, the job name is the sole selector. A worker must have every required selector as a label (plus the automatic worker label from workers.json).

Manifest Effective selectors Worker labels needed
(empty) prometheus prometheus, worker
"selectors": ["worker"] worker worker
"selectors": ["worker", "prod"] worker, prod worker, prod

Dedicated jobs (for example prometheus) can omit selectors when the target worker is labeled with the job name:

{}

Label that worker in workers.json:

{ "host": "10.0.0.5", "labels": ["prometheus"], "memory": "4 gb", "cpu": "2 ghz" }

Shared pool jobs use "selectors": ["worker"]. Environment-specific jobs add labels such as "prod" / "staging" — see resources-and-placement.md.

Minimum allocations

Set min_allocations_count when a job must land on at least N workers after label matching. Build counts non-removed allocation rows (disabled allocations still count) and fails with ErrInsufficientAllocations when the count is lower than the manifest minimum. Omit the field or set 0 for no minimum.

{
  "selectors": ["worker"],
  "min_allocations_count": 2
}

Typical use: HA services that need replicas across zones — pair with worker tags.zone and enough workers per zone in workers.json.


Version

version identifies the job release for KV, templates, dependency checks, and deploy upgrade tracking.

Build target vs running version

Layer When Where Meaning
Target maand build job.version, maand/job/<job>/version From manifest (0.0.0 when omitted)
Per allocation build / deploy hash.current_version, allocations.new_version Running vs build target (catalog)
Templates (.tpl) deploy {{ .CurrentVersion }}, {{ .NewVersion }} Running vs target on allocation context

After deploy promote (per batch during rollout), catalog current_version becomes new_version. When the job defines post_deploy hooks, deploy is complete only after post_deploy_status = success. Job-level KV maand/job/<job>/version holds the build target. First deploy starts at current_version = 0.0.0.

Worker make start/restart and hooks receive CURRENT_VERSION and NEW_VERSION. Details: cli/deploy.md.

Format

Input Parsed as
"1.0.0" 1.0.0
"v2.1" 2.1.0
"3" 3.0.0
"2.0.0-rc1" 2.0.0-rc1

Invalid: empty, unknown, 1.2.3.4, non-numeric segments.

When required

Job type version in manifest
Standalone (no demands) Optional (stored as 0.0.0)
Has demands on a command Required
Upstream job demanded by another Required

Build validates demands.config.min_version / max_version against upstream version. See deployment-sequence.md.

Inspect

maand cat kv get maand/job/database version
maand cat deployments --jobs api
maand cat certs --jobs api

Commands block

Commands live in manifest.jsoncommands and as scripts under _hooks/.

Rule Detail
Name Must start with hook_
Script Exactly one of hook_<name>.py, .ts, or .js
executed_on One or more allowed events (see cli/hooks.md)
demands Optional upstream job/hook dependency — deployment-sequence.md

Allowed executed_on values:

post_build, pre_deploy, post_deploy, job_control, health_check, cli, after_allocation_started, after_allocation_stopped

Each (command, executed_on) pair becomes one row in hooks.

Empty dependency (default):

"demands": { "job": "", "hook": "", "config": {} }

Deploy rollout

These fields control how deploy applies an upgrade after files are rsynced. They do not affect build or placement. Full behavior: cli/deploy.md.

restart_policy

Value Meaning
always (default) Run make restart on every content or version upgrade
reload Run make reload when possible; pair with restart_globs for paths that need a full restart
never Rsync and promote only; no Makefile lifecycle on upgrade

New allocations always run make start, regardless of policy.

restart_globs

Optional string array. Valid only when restart_policy is reload. Each entry is a glob relative to the job directory (Makefile, bin/**, docker-compose.yml, etc.).

Maand diffs per-file content hashes between the staged tree and the last promoted tree. If any changed path matches a glob, deploy runs restart instead of reload on that allocation.

Example:

{
  "restart_policy": "reload",
  "restart_globs": ["docker-compose.yml", "Dockerfile", "bin/**"]
}

Requires a reload: target in the Makefile (and restart: for glob-triggered full restarts).


Rollout fields (summary)

Field / KV Purpose Guide
max_concurrent_starts Batch size for first deploy starts guides/rolling-deploy
max_concurrent_upgrades Batch size for restart / reload upgrades guides/rolling-deploy
restart_policy always / reload / never on updated allocations cli/deploy.md
restart_globs Critical paths when policy is reload cli/deploy.md
rollout_order KV Worker order within batches (build-synced; override via put_rollout_order in pre_deploy or cli) kv/namespaces.md