Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Configuration Reference

Procman reads a single .pman file (passed as a positional argument). The file contains top-level blocks in any order.

Top-Level Structure

config {
  logs = "./my-logs"
}

env {
  RUST_LOG = args.log_level
}

arg port {
  type = string
  default = "3000"
  short = "p"
  description = "Port to listen on"
}

job migrate {
  run "db-migrate up"
}

service web {
  env PORT = args.port
  run "serve --port $PORT"
}

service worker if args.enable_worker {
  run "worker-service start"
}

task test_suite {
  run "pytest tests/"
}

event recovery {
  run "./scripts/recover.sh"
}

A .pman file may contain:

  • config { } — global settings (logs, log_time)
  • arg name { } — CLI argument declaration
  • env { } / env KEY = expr — global environment variable bindings
  • job name { } — a one-shot process that runs to completion
  • job name if expr { } — a conditionally evaluated one-shot job
  • service name { } — a long-running daemon process
  • service name if expr { } — a conditionally evaluated service
  • task name { } — an on-demand one-shot process (does not auto-start; triggered via -t/--task)
  • task name if expr { } — a conditionally evaluated task
  • event name { } — a dormant process, only started via on_fail spawn

Process I/O defaults

  • stdin: redirected to /dev/null. Procman puts every child in its own process group, so an inherited TTY would be a background-pgid read and raise SIGTTIN. If you need to feed input to a process, plumb it through the run string itself (e.g. run "source | program").
  • stdout / stderr: captured (combined into one stream) and written to the per-process and combined log files; ANSI escapes are stripped from log files only.

Output formatting

  • Per-process prefix: every line on stdout is prefixed with the originating job/service name, right-aligned and padded for visual scan.
  • Color: on a TTY, the prefix is colored using a deterministic hash of the process name (so the same name always gets the same color across runs). Set the NO_COLOR environment variable (any non-empty value) to disable.
  • Log files stay plain: ANSI escape sequences from the child process and the prefix coloring are both stripped from the per-process and combined log files.
  • Startup messages: at startup, procman prints to stderr the resolved (canonicalized, absolute) log directory and each per-process log file path, so log locations are unambiguous.

Identifiers

Job names, event names, arg names, and variable names are identifiers. Valid identifiers match [a-zA-Z_][a-zA-Z0-9_-]* — they start with a letter or underscore, followed by letters, digits, underscores, or hyphens.

Reserved Keywords

The following identifiers are reserved by the language and cannot be used as job, service, task, event, arg, or for/var names:

  • module — used in the module.dir built-in.
  • procman — used in the procman.dir built-in.

Other reserved tokens (job, service, task, event, config, env, arg, import, as, wait, watch, for, if, in, on_fail, run, true, false, none) are also unavailable as identifiers, but module and procman are called out here because they look like ordinary names yet are reserved for built-in directory references (see Language Design).

String Literals

String literals are double-quoted. Supported escape sequences: \" (literal quote), \\ (literal backslash), \n (newline), \t (tab). No other backslash escapes are recognized.

Duration Literals

Duration literals are a number followed by a unit suffix: s (seconds), ms (milliseconds), m (minutes). Fractional values are allowed (e.g., 1.5s).

The none Literal

none represents the absence of a value. It is valid only in specific positions: timeout = none (infinite wait), default = none (no default). Using none in env value positions or boolean contexts is a parse-time error.

config { } Block

Global settings applied to all jobs.

config.logs

Optional log directory path. Defaults to logs/procman. Recreated each run.

config {
  logs = "./my-logs"
}

env { } Block

Global environment variable bindings applied to all jobs. Overridable per-job. Declared at the top level.

Block form:

env {
  RUST_LOG = args.log_level
}

Single-binding form:

env RUST_LOG = args.log_level

Both forms can appear multiple times and coexist in the same file.

arg name { } Block

CLI arguments parsed after --. Declared at the top level. Underscores become dashes on the CLI (log_level--log-level).

arg port {
  type = string
  default = "3000"
  short = "p"
  description = "Port to listen on"
}

arg enable_feature {
  type = bool
  default = false
}
FieldRequiredDefaultDescription
typenostringstring or bool
shortnoSingle character shorthand
descriptionnoHelp text for -- --help
defaultnoFallback value. Args without a default are required.

Arg values are referenced in expressions as args.name. There is no env field on args — use a top-level env { } block to explicitly bind args to environment variables.

Running procman myapp.pman -- --help prints generated usage based on the arg definitions.

Env Precedence

Lowest to highest:

SourcePriority
System env (inherited)lowest
CLI -e KEY=VALUE flags
Top-level env { }
Per-job env
Per-iteration for bindingshighest

Note: var bindings from contains conditions are procman expressions, not direct env injections. They enter the environment only when explicitly assigned via env KEY = var_name.

job and service Blocks

Each job block defines a one-shot process (runs to completion). Each service block defines a long-running daemon process. Both share the same fields.

run (required)

The command to execute. All commands are passed to bash -euo pipefail -c, so shell features like pipes, redirects, &&, variable expansion, and multi-line scripts all work naturally. bash must be on PATH.

Inline form:

run "echo hello"

Multi-line fenced form:

run """
  ./run-migrations
  echo "DATABASE_URL=postgres://localhost:5432/mydb" > $PROCMAN_OUTPUT
"""

Procman never interpolates inside shell strings. Values flow in exclusively via environment variables.

An empty or whitespace-only run value is rejected at parse time.

env (optional)

Environment variables merged into the job’s environment. Single-binding and block forms can coexist:

service api {
  env DB_URL = @migrate.DATABASE_URL
  env {
    API_KEY = "secret"
    LOG_DIR = args.log_dir
  }
  run "start-api --db $DB_URL"
}

job vs service

A job is a one-shot process that runs to completion. Exit code 0 is treated as success and does not trigger supervisor shutdown. A non-zero exit code still triggers shutdown. Jobs can write key-value output to $PROCMAN_OUTPUT, which other processes reference via @job.KEY expressions (see Process Output).

A service is a long-running daemon process. If a service exits, it triggers supervisor shutdown.

task

A task is a one-shot process that does not auto-start. Tasks must be explicitly triggered via the -t/--task CLI flag. Like jobs, tasks run to completion — exit code 0 is success, non-zero triggers shutdown.

Tasks are useful for operations that should only run on demand: test suites, database migrations, cleanup scripts, or any one-shot utility work.

task test_suite {
  wait {
    after @migrate
  }
  run "pytest tests/"
}

Trigger with: procman myapp.pman -t test_suite

Tasks share all the same fields as jobs and services (run, env, wait, for, if, watch).

wait (optional)

A block of conditions that must all be satisfied before run executes. See the Dependencies chapter for the full reference.

wait {
  after @migrate
  http "http://localhost:3000/health" {
    status = 200
    timeout = 30s
    poll = 500ms
  }
}

if (optional)

An expression on the job or service line. If falsy, the job/service is not evaluated at all — no dependency waiting, no env resolution. Skipped jobs still register as exited so after dependents can proceed.

service worker if args.enable_worker {
  run "worker-service start"
}

watch (optional)

Named runtime health check blocks that monitor a service after it starts. See the Dependencies chapter for condition syntax.

service web {
  run "web-server --port 8080"

  watch health {
    http "http://localhost:8080/health" {
      status = 200
    }
    initial_delay = 5s
    poll = 10s
    threshold = 3
    on_fail shutdown
  }
}

for (optional)

Iteration block that wraps env and run, spawning one instance per element. See the Fan-out chapter for full details.

job nodes {
  for config_path in glob("configs/node-*.yaml") {
    env NODE_CONFIG = config_path
    run "start-node --config $NODE_CONFIG"
  }
}

event name { } Block

Event handlers are declared at the top level. They are never auto-started — they are spawned via on_fail spawn @name in a watch block.

event recovery {
  run "./scripts/recover.sh"
}

on_fail spawn @name must reference an event, not a job.

Shell Blocks

Procman never interpolates inside shell strings. Values flow into shell exclusively via environment variables set with env bindings.

Inline:

run "echo hello"

Multi-line fenced:

run """
  ./run-migrations
  echo "DATABASE_URL=postgres://localhost:5432/mydb" > $PROCMAN_OUTPUT
"""

Expression Language

Expressions appear in if conditions, env value positions, and var bindings. They are never evaluated inside shell strings.

Value References

SyntaxDescription
args.nameCLI arg value
@job.KEYOutput from a job’s PROCMAN_OUTPUT
local_varJob-scoped variable (from for or var binding)

Literals

TypeExamples
String"hello", "3000"
Number42, 3.14
Booltrue, false
Duration5s, 500ms, 2m
Nonenone

Operators

CategoryOperators
Comparison==, !=, >, <, >=, <=
Logical&&, ||, !
Grouping( )

No arithmetic in v1.

Type Errors

Type errors in expressions cause immediate procman runtime panic and shutdown. There is no silent coercion. A type error is a bug in the config.

Parse-Time Validation

Procman validates the configuration at parse time and exits with an error (with file:line:col location) if any of these checks fail:

  • Syntax errors — malformed blocks, missing fields, invalid tokens
  • Unknown identifiers — referencing an arg or job that doesn’t exist
  • after @job targets — must reference a job (not a service)
  • @job.KEY references — must point to a job (not a service) and require after @job in the process’s wait block (direct or transitive)
  • Circular dependencies — cycles in after references
  • on_fail spawn @name — must reference an event
  • Variable shadowing — reusing a name already bound by for, var, or args
  • Empty run commands — rejected at parse time