Skip to content

Format Reference

The runspec format is TOML. It is identical across Python and Node — only the install step differs. This page is the working reference; the canonical spec lives at spec/SPEC.md.


File location

runspec.toml lives inside your package directory, alongside the code it describes:

mypkg/
  __init__.py
  runspec.toml    ← here, not at the project root

This location means build backends include it automatically as package data, and importlib.metadata / node_modules/.bin discovery can locate it after install with no extra configuration.

File lookup: parse() walks up from cwd to find the first runspec.toml. For server contexts, RUNSPEC_CONFIG=/abs/path/to/runspec.toml overrides the walk — runspec serve sets it automatically when spawning subprocesses.


Top-level structure

[config]     # optional project-wide defaults
[<name>]     # one section per runnable — anything that's not `config`

config is the only reserved name. Every other top-level section defines a runnable. The section name is what users type on the command line.


The [config] section

Project-wide defaults. All fields optional.

Field Type Default Description
autonomy-default string "confirm" Autonomy when unspecified on a runnable
lang string Preferred language for runspec init code stubs
name string venv dir name MCP server name reported by runspec serve
version string "1" runspec spec version
jump-hosts table Per-alias jump host config. See Jump Hosts.
logging table Logging configuration. See Logging.
[config]
autonomy-default = "confirm"
lang             = "python"
version          = "1"

[config.jump-hosts] and [config.logging] get their own pages — they're substantial enough that documenting them inline would bury the rest of the reference.


Runnable definition

Every top-level section except [config] is a runnable.

[deploy]
description     = "Deploy to production"       # recommended
autonomy        = "manual"                     # optional
autonomy-reason = "Irreversible operation"     # optional
output          = "text"                       # optional
discoverable    = true                         # optional, default true; also false or ["operator","self-service"]
hosts           = ["web-01", "web-02"]         # optional, see Remote Execution
run_as          = "deploy"                     # optional, see Remote Execution
become_method   = "sudo"                       # optional, default "sudo"
become_flags    = "-H"                         # optional
examples        = [...]                        # optional, see below

description

Shown in --help and in every emitted agent schema. Strongly recommended — agents lean on this to decide whether a tool fits the task.

autonomy levels

Declares how much trust an agent runtime should have when invoking this runnable. It's a contract for agent invocation, not a directive for human users — a human typing the command has already chosen the action.

Level Meaning
autonomous Run freely — no confirmation needed
confirm Present the planned call, wait for human approval
supervised Run, then show the result before acting on it
manual Never invoke — human only

Falls back to [config] autonomy-default, then "confirm".

output

Declares what the runnable writes to stdout. Surfaces as x-output in every emitted schema so agents know whether to display or parse the response.

Value Meaning
text Human-readable output (default)
json Structured JSON — agent can parse and act on it
html HTML output (reserved for future UI use)

discoverable

Declares which audiences may discover the runnable. Where serve controls the context (local vs remote), discoverable controls who — they compose.

Value Audiences Meaning
true (default) ["operator"] Operator-visible (runspec serve/local). Not in any self-service catalog.
false [] Hidden from all discovery — an internal helper.
["operator", "self-service"] as written Operator-visible and offered in the visitor catalog.
["self-service"] as written Only in the visitor catalog — hidden from runspec serve/local.

The self-service audience is opt-in: a runnable reaches a visitor-facing catalog only by listing it explicitly, so the default never exposes a runnable to untrusted users. runspec serve skips runnables that aren't operator-discoverable (the way to hide an internal helper from the agent surface); runspec local still lists everything and carries the canonical audience list as x-discoverable in emitted schemas. Metadata only — it adds no runtime behaviour to the runnable.

examples

A list rendered by --help. Inline TOML tables with cmd (required) and optional description:

[mytool]
examples = [
  {cmd = "mytool",                description = "Run with defaults"},
  {cmd = "mytool --verbose",      description = "Show debug output"},
  {cmd = "mytool --input data.csv"},
]

Bare strings are accepted as shorthand: examples = ["mytool", "mytool --verbose"]. Examples are declarative — the spec layer never executes them.


Argument definition

Arguments live under [<name>.args].

Three levels of verbosity

Bare value — type and default inferred:

[greet.args]
verbose = false    # flag, default false
workers = 4        # int, default 4
label   = "main"   # str, default "main"

Inline table — explicit fields on one line:

[greet.args]
input-dir = {type = "path"}
quality   = {default = 85, range = [1, 100]}
format    = {options = ["json", "csv"], default = "json"}

Full block — for args that need prose descriptions or many fields:

[greet.args.quality]
default     = 85
range       = [1, 100]
description = "Output quality. Values below 60 are rarely useful."

Argument fields

Field Type Description
type string Argument type. Inferred from default or options if omitted.
default any Default value. Absence implies required = true.
required bool Override the inferred required flag.
description string Shown in --help and emitted schemas.
options array Valid choices. Infers type = "choice".
range [min, max] Valid range for numeric types.
pattern string Regex the value must fully match. str type only.
min-length int Minimum string length. str type only.
max-length int Maximum string length. str type only.
multiple bool Accept multiple values (repeated-flag style).
delimiter string Split a single value by this character.
short string Short flag alias, e.g. "-v". Must be unique within a runnable; -h is reserved.
env string | array Environment variable fallback(s), checked after the CLI and before the default.
deprecated string Deprecation message shown on use.
autonomy string Per-arg autonomy override. Most restrictive level wins.
discoverable bool | array Audiences that see this arg on the visitor self-service form. Default-deny — listed only when it contains "self-service". See Visitor-form curation.
ui string Form control hint. Inferred from type if omitted.
hint string Placeholder text for a UI form field. Advisory for str/path/password; never affects parsing.
meta table Developer-defined pass-through metadata. runspec never reads it.
position int 1-based positional index. Makes the arg positional rather than a --flag.

Positional arguments

Set position = N to make an arg positional. Positions must be unique within a runnable:

[deploy.args]
target  = {type = "str", description = "Target host", position = 1}
release = {type = "str", description = "Release tag", position = 2, required = false}
deploy prod v1.2.3       # target=prod, release=v1.2.3
deploy prod              # target=prod, release=None

String validation (pattern, min-length, max-length)

str args can declare format constraints. pattern is a regex the value must fully match (re.fullmatch semantics — anchored at both ends, so you don't need ^/$); min-length/max-length bound the character count:

[ticket.args]
jira-key = {type = "str", pattern = "[A-Z]+-[0-9]+", description = "e.g. PROJ-123"}
slug     = {type = "str", min-length = 3, max-length = 40}
ticket --jira-key PROJ-123     # ok
ticket --jira-key proj-123     # ✗  Invalid value for --jira-key: 'proj-123'
                               #    Expected: a value matching pattern '[A-Z]+-[0-9]+'

These apply to the str type only and are ignored for other types. When you emit a schema (runspec local --format mcp), they map to the native JSON Schema keywords pattern, minLength, and maxLength, so MCP hosts and survey forms enforce them too.

Availability

String validation was added in runspec 0.22.0 and node-0.18.0. The Node pack anchors pattern as ^(?:…)$ to match Python's re.fullmatch exactly, including patterns with a top-level alternation.

Secret arguments (password)

The password type marks an argument whose value is a secret — a password, token, or API key. It coerces and validates exactly like str (so pattern / min-length / max-length still apply), but it is handled differently everywhere a value could leak:

[deploy.args]
db-password = {type = "password", description = "Database password"}
api-token   = {type = "password", env = "DEPLOY_API_TOKEN", required = false}
  • Refused on the command line. deploy --db-password hunter2 is a hard error — a secret on the CLI lands in shell history and the process table (ps). The value must come from the environment instead.
  • Resolved from the environment. The automatic RUNSPEC_<RUNNABLE>_ARG_<NAME> variable, a declared env alias, or .runspec_env — the normal resolution chain minus the CLI tier.
  • Prompted interactively. When a required password is still missing and the command is run in a terminal, runspec prompts for it without echoing input. Non-interactive and agent runs don't prompt.
  • Omitted from agent schemas. runspec local --format mcp (and the other agent formats) leave password args out of inputSchema, so an agent is never asked to supply a secret.
  • Kept out of logs. The value is redacted from the run-summary audit record.
deploy --db-password hunter2              # ✗  refused
RUNSPEC_DEPLOY_ARG_DB_PASSWORD= deploy   # ok — from the environment
deploy                                    # prompts "db-password:" (no echo)

In runspec-console, a password arg renders as a masked form field; the value you type is sent to the runnable as the RUNSPEC_<RUNNABLE>_ARG_<NAME> environment variable (local subprocess env, or the SSH command's env prefix for remote hosts) — never as a command-line argument. No persistent secret store is involved; the value is per-invocation and transient.

Availability

The password type was added in runspec 0.30.0 and node-0.27.0.

Form hints (hint)

The hint field supplies placeholder text for a UI form field — an example value or format reminder shown in the empty control. It is advisory: runspec never reads it during parsing or validation, and it has no effect on the command line. It pairs naturally with str, path, and password args:

[deploy.args]
host        = {type = "str",      hint = "web-01.prod.example.com"}
config      = {type = "path",     hint = "./deploy.yaml"}
db-password = {type = "password", hint = "from the vault"}

runspec-console renders hint as the input's placeholder.

Availability

The hint field was added in runspec 0.30.0 and node-0.27.0.

Visitor-form curation (discoverable)

discoverable at the runnable level decides whether a runnable reaches a visitor-facing self-service catalog at all. Set it on an argument to curate which of that runnable's args appear on the visitor's generated form — without hiding anything from operators or the agent.

The arg level consults only the self-service audience, and it is default-deny: an arg appears on the visitor form only when its discoverable explicitly lists "self-service". Operators and the agent always see every argument (they enact the runnable; an input can't be hidden from the caller), so a bare true, false, or ["operator"] all keep an arg off the visitor form.

[request-vm]
description  = "Provision a VM for the caller"
discoverable = ["operator", "self-service"]    # the runnable is in the catalog

[request-vm.args]
size          = {options = ["small", "medium", "large"], discoverable = ["self-service"]}
region        = {default = "eu-west-1",                   discoverable = ["self-service"]}
justification = {type = "str",                            discoverable = ["self-service"]}
project_id    = {type = "str"}        # operator-only: off the visitor form
force         = {type = "flag"}       # operator-only: off the visitor form

The visitor sees a three-field form (size, region, justification). The operator-only args are resolved when the request is enacted — from their default, or supplied by the operator/agent at the autonomy gate. (A password arg is always excluded from visitor forms regardless of discoverable.)

Value On the visitor form? Operator sees it?
absent no (default-deny) yes
["self-service"] yes yes
["operator", "self-service"] yes yes
["operator"] / true / false no yes

The emitted arg schema carries the canonical audience list as x-discoverable when set; the self-service catalog publisher filters on it.

Availability

Arg-level discoverable was added in runspec 0.35.0. Node parity is tracked separately.

Multiple values (multiple, delimiter)

multiple = true collects repeated flags (--tag a --tag b) into a list; add delimiter to also split a single value (--fields a,b,c). The arg's type is applied to each item, and the parsed value is a list of coerced itemsmultiple with type = "int" yields [1, 2, 3], not strings.

Per-item coercion means every per-item check runs on each element: pattern / min-length / max-length for str, range for numbers, options for choice. When items fail, the error reports which ones by position and value, validating the whole list before reporting:

[deploy.args]
ticket = {type = "str", multiple = true, pattern = "[A-Z]+-[0-9]+"}
deploy --ticket PROJ-1 --ticket bad-2 --ticket nope
# ✗  --ticket: 2 of 3 item(s) failed validation:
#
#    • item 2 ('bad-2'):
#      ✗  Invalid value for --ticket: 'bad-2'
#         Expected: a value matching pattern '[A-Z]+-[0-9]+'
#         Got: 'bad-2'
#    • item 3 ('nope'):
#      ...

Availability

Per-item coercion and validation for multiple args landed in runspec 0.24.0 and node-0.19.0. (Earlier versions stringified the list instead of coercing each element.)

Pass-through arguments (type = "rest")

One arg per runnable can have type = "rest". It captures everything after a literal -- token as a list of strings — useful when wrapping another command:

[wrap.args]
extra = {type = "rest", description = "Args passed to the wrapped command"}
wrap -- --foo bar --baz       # extra = ["--foo", "bar", "--baz"]
wrap                          # extra = []

rest args are never required and default to []. This is exactly how runspec jump <alias> <tool> -- <tool-args…> works.


Inference rules

When fields are omitted, runspec infers them. Rules apply in this order:

Condition Inference
options = [...] present type = "choice"
default = true or false type = "flag"
default = <integer> type = "int"
default = <float> type = "float"
default = <string> type = "str"
No default, no required = false required = true
type = "path" with no default required = true

Note

options is checked before default — if both are present, type = "choice" wins. Bool is checked before int — false and true are flags, not integers.

The canonical definition of these rules lives in spec/SPEC.md; every language pack is tested against it, and the table above mirrors it.


Types

Type Description
str Unicode string
password Secret string — masked in UIs, refused on the command line, omitted from agent schemas, redacted from logs. See Secret arguments.
int Integer
float Floating point number
bool Boolean (true / false)
flag Boolean switch — presence means true
path File system path
choice One of a declared set of options
rest List of strings captured after a literal -- separator. At most one per runnable.

Language packs may register additional types. See register_type() / registerType().


Value resolution order

For every argument, values are resolved in this order:

  1. Explicit CLI argument — highest priority
  2. Environment variable (if env declared)
  3. Config file value (future)
  4. Default from spec
  5. Error: required — if nothing matched and required = true

Environment variable fallbacks

The env field lets an argument read from an environment variable when nothing is passed on the command line:

[deploy.args]
server  = {type = "str",  env = "DEPLOY_SERVER"}
api-key = {type = "str",  env = "DEPLOY_API_KEY",  autonomy = "manual"}
region  = {type = "str",  env = "AWS_REGION",       default = "us-east-1"}
dry-run = {default = false}

CI/CD pattern

Set variables once at the project level — your pipeline file stays identical across every project, and each project controls its own behaviour through environment variables:

# .gitlab-ci.yml — shared across all projects
deploy:
  script: deploy

In Project → Settings → CI/CD → Variables, set:

DEPLOY_SERVER  = web-01
DEPLOY_API_KEY = <secret>
AWS_REGION     = eu-west-1
- name: Deploy
  run: deploy
  env:
    DEPLOY_SERVER: web-01
    DEPLOY_API_KEY: ${{ secrets.DEPLOY_API_KEY }}
    AWS_REGION: eu-west-1
- name: Deploy application
  command: deploy
  environment:
    DEPLOY_SERVER: "{{ deploy_server }}"
    DEPLOY_API_KEY: "{{ vault_deploy_api_key }}"
    AWS_REGION: "{{ aws_region }}"

Tip

Combine env with autonomy = "manual" for secrets. The arg is still readable from the environment, but agents are blocked from passing it — the value has to come from the operator.

Runtime env injection

When a runnable is invoked via runspec serve or runspec jump, every resolved argument is also exported as RUNSPEC_<ARG_NAME_UPPERCASED> before the process starts. Bash, Python, Node, or anything else can read those without a language-specific library. Hyphens become underscores; flag/bool values become 0/1; multiple = true lists are newline-delimited. RUNSPEC_AGENT=1 is set on every serve invocation.


Groups

Groups define relationships between arguments and are validated after individual args pass.

[<name>.groups.<group-name>]
exclusive = true
args      = ["format", "raw"]

Group types

Exclusive — at most one arg from the group may be provided:

[pipeline.groups.output-format]
exclusive = true
args      = ["format", "raw"]

Inclusive — if any arg is provided, all must be:

[pipeline.groups.auth]
inclusive = true
args      = ["api-key", "api-endpoint"]

At least one — one or more from the group must be provided:

[pipeline.groups.input]
at-least-one = true
args         = ["input-file", "input-dir", "input-glob"]

Exactly one — strictly one must be provided:

[pipeline.groups.mode]
exactly-one = true
args        = ["fast", "balanced", "quality"]

Conditional — if a trigger arg is provided, other args become required:

[pipeline.groups.upload]
if       = "upload"
requires = ["bucket", "region"]

Subcommands

Subcommands live under [<name>.commands.<subcommand>]. Each has its own args, groups, autonomy, description, and examples:

[pipeline]
description = "Process and validate data pipeline files"
autonomy    = "confirm"

[pipeline.commands.run]
description     = "Run the pipeline"
autonomy        = "confirm"
autonomy-reason = "Writes output files and may call external APIs"

[pipeline.commands.run.args]
input   = {type = "path"}
dry-run = {default = false}

[pipeline.commands.validate]
description = "Validate without running"
autonomy    = "autonomous"

[pipeline.commands.validate.args]
input  = {type = "path"}
strict = {default = false}

runspec serve flattens nested subcommands into MCP tools with underscore-joined names (e.g. portal-api_orders_get-list).

Requiring a subcommand (require-command)

By default a runnable with subcommands can still run "bare" (no subcommand). Set require-command = true on the parent to make choosing one mandatory — the equivalent of argparse's add_subparsers(required=True), Click groups, or git / docker / kubectl:

[db]
description     = "Database administration"
require-command = true        # `db` on its own is an error

[db.commands.migrate]
description = "Apply pending migrations"

[db.commands.seed]
description = "Seed the database with fixtures"

Invoking the level with no command — or a token that is not one of its commands — errors with the list of available commands (a mistyped token also gets a Did you mean suggestion). It is a parent-level flag and applies at any nesting depth: a nested command that itself declares require-command enforces its own children. Enforcement happens only when parsing real CLI arguments — load_spec() / loadSpec() introspection and schema emission are unaffected, so agent tooling still sees the runnable.

Availability

require-command was added in runspec 0.23.0 and node-0.18.0.


Logging configuration

When [config.logging] is present, parse() configures stdlib logging automatically and auto-injects a --debug flag. The full reference is on the dedicated Logging page; the short form:

[config.logging]
rotate = "midnight"    # daily | midnight | weekly | "10 MB" | "1 GB" | …
keep   = 7

File logs go to {package_dir}/logs/{runnable}.log as JSON at DEBUG, with midnight rotation and 7-day retention. Sensitive data is redacted on every log line. See Logging for the full picture.


Remote execution

These fields control how runspec jump runs a tool on a remote host. The full reference — including all four run_as shapes — is on the Jump Hosts page.

hosts

Restricts a runnable to specific machines. runspec serve checks the current hostname at startup; tools that don't match are excluded from the MCP tool list. Absent means available everywhere.

[parse-app-logs]
description = "Parse and summarise application logs"
autonomy    = "confirm"
hosts       = ["logserver-01", "logserver-02"]

run_as, become_method, become_flags

Privilege escalation for remote execution. Four run_as shapes are supported (plain string, env var, per-host map, regex patterns):

[deploy]
run_as        = "oracle"
become_method = "sudo"      # default — also: su, pbrun, dzdo
become_flags  = "-H"
# Per-host map
run_as.default = "oracle"
run_as.hosts."special-box-01" = "dba"
run_as.hosts."special-box-02" = ""        # no escalation on this host

# Regex patterns (top to bottom, first match wins)
run_as.default = "oracle"
run_as.patterns."[lg]pexp[0-9]*" = "orasvc"
run_as.patterns."prod[0-9]*"     = "produser"

See Jump Hosts for the full resolution table and the remote-command construction rules.

enforce_run_as

run_as is applied by an executor (runspec serve, an SSH client, the console) before the process starts, so under one the runnable is already the target user. When a runnable is invoked directly — its installed entry point run by hand, from cron, or imported — nothing escalates and nothing checks who is running it. enforce_run_as closes that gap: on parse(), the resolved run_as user is compared (by uid) to the current process's effective user.

[deploy]
run_as         = "oracle"
enforce_run_as = "error"    # default — also: "warn", "off"
Value On mismatch
"error" (default) parse() exits non-zero — the runnable does not run.
"warn" A warning is written to stderr; the runnable continues.
"off" No check.

It is a no-op when run_as resolves to the empty string (no escalation intended) or on a platform with no POSIX uid model (e.g. Windows). The default can be set project-wide via [config] enforce_run_as; a runnable's own value wins.


Developer metadata

The meta field attaches arbitrary structured data to any argument. runspec passes it through untouched — never validated, never interpreted.

A common use case is associating choice values with lookup data needed at runtime:

[deploy.args.server]
options = ["web-01", "web-02", "db-01"]

[deploy.args.server.meta]
web-01 = {datacenter = "us-east", tier = "web"}
web-02 = {datacenter = "us-west", tier = "web"}
db-01  = {datacenter = "eu-central", tier = "db"}
args = parse()
info = args.server.meta[args.server.value]
print(info["datacenter"])   # "us-east"

Lookup data lives in the same place as the argument definition — no separate config files, no hardcoded mappings.

For a small, flat metadata bag, an inline TOML table keeps it on one line with the argument definition — no separate [...meta] section needed:

[deploy.args.timeout]
default = 30
meta = { unit = "seconds", help-url = "https://wiki/timeouts" }
args = parse()
print(args.timeout.meta["unit"])   # "seconds"

The section form and the inline form are equivalent — runspec treats meta as pass-through either way. Reach for inline when the table is short; use a [...meta] section when it spans many keys or nests deeply.

Combining two values

meta is keyed by a single argument's value, and runspec does no interpolation — there is no "${region}-${tier}" template syntax. When a value depends on two arguments, compose the lookup in your own code. Nest the table and index it twice:

[deploy.args.region.meta]
us-east = {web = "10.0.1.0/24", db = "10.0.2.0/24"}
us-west = {web = "10.1.1.0/24", db = "10.1.2.0/24"}
args = parse()
subnet = args.region.meta[args.region.value][args.tier.value]

Or build a composite key from both values:

[deploy.args.target.meta]
"us-east|web" = {subnet = "10.0.1.0/24"}
"us-east|db"  = {subnet = "10.0.2.0/24"}
args = parse()
info = args.target.meta[f"{args.region.value}|{args.tier.value}"]

Because meta is pure pass-through, any composition — string formatting like f"View {args.jira_key.value}", joining multiple values, computed defaults — happens in your runnable, not in runspec.toml.


Complete example

A realistic runnable exercising most features. This is the tests/integration/fixtures/complex.toml shared compliance fixture — every language pack must produce identical schemas from it.

[config]
autonomy-default = "confirm"
lang             = "python"
version          = "1"

[pipeline]
description = "Process and validate data pipeline files"
autonomy    = "confirm"

[pipeline.commands.run]
description     = "Run the pipeline against one or more input files"
autonomy        = "confirm"
autonomy-reason = "Writes output files and may call external APIs"

[pipeline.commands.run.args]
input      = {type = "path"}
tag        = {type = "str", multiple = true}
fields     = {type = "str", multiple = true, delimiter = ","}
format     = {options = ["json", "csv", "parquet"], default = "json"}
workers    = {default = 4, range = [1, 32]}
batch-size = {default = 1000, range = [1, 100000]}
dry-run    = {default = false}
verbose    = {default = false, short = "-v"}
strict     = {default = false}
api-key    = {type = "str", env = "PIPELINE_API_KEY", autonomy = "manual"}
timeout    = {default = 30, range = [1, 300]}
threads    = {default = 4, deprecated = "use --workers instead"}

[pipeline.commands.run.groups.input-format]
exclusive = true
args      = ["format", "raw"]

[pipeline.commands.run.groups.api-auth]
inclusive = true
args      = ["api-key", "api-endpoint"]

[pipeline.commands.validate]
description = "Validate pipeline config and input files without running"
autonomy    = "autonomous"

[pipeline.commands.validate.args]
input  = {type = "path"}
schema = {type = "path"}
strict = {default = false}
format = {options = ["json", "csv", "parquet"], default = "json"}

What it exercises, top to bottom:

  • A [config] block with a project-wide autonomy-default.
  • Two subcommands (run, validate) with separate args and groups.
  • A path-typed required arg (input), inferred required by the type = "path" rule.
  • multiple = true (the --tag flag may repeat).
  • multiple = true and delimiter (the --fields flag splits one value).
  • A choice arg (format) — inferred from options.
  • range on numeric args (workers, batch-size, timeout).
  • A flag with a short alias (verbose / -v).
  • An env fallback on a secret arg (api-key) combined with autonomy = "manual" to block agent invocation.
  • A deprecated arg (threads) — usable but emits a warning.
  • An exclusive group (input-format) and an inclusive group (api-auth).
  • A subcommand-level autonomy override (validate is autonomous even though the parent is confirm).

runspec local --format mcp against this spec emits two tools (pipeline_run and pipeline_validate) with full input schemas, autonomy levels, and the inclusive/exclusive constraints described inline.