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 declarationenv { }/env KEY = expr— global environment variable bindingsjob name { }— a one-shot process that runs to completionjob name if expr { }— a conditionally evaluated one-shot jobservice name { }— a long-running daemon processservice name if expr { }— a conditionally evaluated servicetask name { }— an on-demand one-shot process (does not auto-start; triggered via-t/--task)task name if expr { }— a conditionally evaluated taskevent name { }— a dormant process, only started viaon_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 therunstring 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_COLORenvironment 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 themodule.dirbuilt-in.procman— used in theprocman.dirbuilt-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
}
| Field | Required | Default | Description |
|---|---|---|---|
type | no | string | string or bool |
short | no | — | Single character shorthand |
description | no | — | Help text for -- --help |
default | no | — | Fallback 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:
| Source | Priority |
|---|---|
| System env (inherited) | lowest |
CLI -e KEY=VALUE flags | |
Top-level env { } | |
Per-job env | |
Per-iteration for bindings | highest |
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
| Syntax | Description |
|---|---|
args.name | CLI arg value |
@job.KEY | Output from a job’s PROCMAN_OUTPUT |
local_var | Job-scoped variable (from for or var binding) |
Literals
| Type | Examples |
|---|---|
| String | "hello", "3000" |
| Number | 42, 3.14 |
| Bool | true, false |
| Duration | 5s, 500ms, 2m |
| None | none |
Operators
| Category | Operators |
|---|---|
| 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 @jobtargets — must reference ajob(not aservice)@job.KEYreferences — must point to ajob(not aservice) and requireafter @jobin the process’swaitblock (direct or transitive)- Circular dependencies — cycles in
afterreferences on_fail spawn @name— must reference anevent- Variable shadowing — reusing a name already bound by
for,var, or args - Empty
runcommands — rejected at parse time