Agent CLI (pipelex-agent)
The pipelex-agent CLI is a machine-oriented companion to the main pipelex CLI. It is designed for programmatic consumption by AI agents, IDE extensions, and other automation tools. Output format varies by command — markdown or JSON, raw TOML, or plxt passthrough — with no Rich formatting or interactive prompts. Structured commands report a failure on stderr as its problem, the error document of the MTHDS standard that the hosted API answers too, in Markdown or in JSON (see Output Contract). The error format is the command's --error-format, else the program's own --error-format given before the command (pipelex-agent --error-format json validate bundle …), else the command's --format (so --format json flips both, as before the split), else JSON. fmt and lint pass through native plxt output.
It is consumed by the mthds-agent CLI (from the mthds npm package) which itself is used by Claude Code skills, the VS Code extension, and can be called directly from the command line.
It ships in the same cli extra as the main CLI, uv tool install "pipelex[cli]": its own output carries no Rich formatting, but the command modules it shares with pipelex import Rich.
Global Options
| Option | Description |
|---|---|
--version |
Print the version handshake — the runtime version, the MTHDS Protocol version and the MTHDS standard version — and exit. Same three lines as pipelex --version; see the CLI reference |
There is no --log-level flag: pipelex-agent is machine-consumed, so logging is cut off process-wide by design.
--runner api is gone
The global --runner pipelex|api option and its MTHDS API runner path are removed. Where a run executes is now a run subcommand option, --runner local|hosted, and the hosted value runs on the hosted Pipelex API through pipelex-sdk, with the key in PIPELEX_API_KEY rather than MTHDS_API_KEY. A self-hosted runner is reached with --runner hosted --base-url <origin>; it keeps no runs, takes no uploads and has no catalog, as Running on the Hosted API describes. To target an arbitrary MTHDS runner, use the vendor-neutral mthds-agent CLI.
Commands Overview
The agent CLI mirrors the main CLI's subcommand structure for run, validate, and inputs, each with pipe, bundle, and method subcommands. It also provides fix bundle for deterministic in-place repairs, and fix function-names to migrate a method's PipeFunc function names.
Run
Execute a pipeline. Success output is markdown by default, or JSON with --format json. Errors follow --error-format (defaults to the program's --error-format, then to --format's value).
pipelex-agent run pipe <PIPE_CODE> [OPTIONS]
pipelex-agent run bundle <PATH> [OPTIONS]
pipelex-agent run method <NAME> [OPTIONS]
Common options:
--inputs,-i- Path to a JSON or TOML inputs file (discriminated by file extension:.toml→ TOML, everything else → JSON). Inline JSON (a value starting with{) stays JSON-only.--dry-run- Dry-run without calling AI providers--mock-inputs- Use mock inputs (requires--dry-run)--graph/--no-graph- Enable/disable execution graph (enabled by default)--library-dir,-L- Additional library directory--with-memory- Include full working memory in output, each stuff naming its concept by ref ("concept": "<domain>.<Code>"); piped back into anotherrunon stdin, the envelope's stuffs become that run's inputs, provided the receiving method declares the same<domain>.<Code>— the ref carries its domain, and a method declaring another one refuses it rather than guessing--format- Success output format:markdown(default) orjson--error-format- Error output format:markdownorjson(defaults to the program's--error-format, then to--format's value)--runner local|hosted- Where the run executes: on this machine, or on the hosted Pipelex API. Defaults to[run] executionin.pipelex/pipelex.toml(see Run Configuration), elselocal--hosted/--local- The same choice as--runner, spelled as the main CLI spells it. Given together, they must agree--base-url <scheme://host[:port]>- The origin a hosted run calls. OverridesPIPELEX_BASE_URL, which overrideshttps://api.pipelex.com; refused on a run that executes locally
For bundle and method, use --pipe to target a specific pipe.
run bundle reads its method through its method tree, as pipelex run bundle does. A folder is the method rooted at the nearest METHODS.toml at or above it, else at the folder itself, and with no --pipe the run starts at the manifest's main_pipe, or, with no manifest, at the one main_pipe its bundles declare; none or several is refused with the problem request.pipe_unspecified, whose message names each bundle by its path in the tree and whose params.candidates names the declared ones when there are several, with no errors, since the method may be valid and only the pipe to start at is missing: pass --pipe. A bundle file named alone starts at its own main_pipe, and with no METHODS.toml at or above its folder its method is that one file, so a step naming a file beside it, such as a template_file, is refused with the advice to run the folder or to add a METHODS.toml. The run's output file (live_run.json or dry_run.json, in the run's output folder) carries method_tree_digest, the digest of the tree the run read, and so does the output under --with-memory: the tree is listed once, before its bundle is read, and the run loads the tree that listing returned, so the digest is the digest of the files the run loaded. A file of the tree edited after that listing is refused as an invalid method whose item names the file, rather than loaded.
Running on the hosted API
With --runner hosted (or --hosted, or [run] execution = "hosted"), the run executes on the hosted Pipelex API and nothing local is booted: no provider key or inference configuration is needed on this machine, only a Pipelex API key in PIPELEX_API_KEY, saved in ~/.pipelex/.env by pipelex login or exported in the shell. A .env file Pipelex loads wins over the shell, and the working directory's wins over ~/.pipelex/.env, so an exported key is read only when neither file sets the variable. What the run sends is what a local run would load, and only its .mthds files, as one method tree, the MTHDS protocol's files map: the method's own bundles under the paths they have in its folder and each library directory beside it under a top-level folder named after it, inline up to 256 KiB and by hash beyond (see Running on the Hosted API). A PipeFunc's Python, structure classes, METHODS.toml and the packages a dependency alias names stay on this machine, so a local bundle relying on them runs with --runner local. run bundle sends the bundle, then every .mthds file of its library directories' method trees, so a bundle the membership rule keeps out is never sent. run pipe sends its library directories' files (the installed method exporting the pipe and -L, else PIPELEXPATH) and is refused when it has none. run method sends an installed or local method's files, but a published address (github.com/owner/repo[/name][@tag]) goes as a method_ref and a stored method's catalog id (mt_…) as a method_id, both resolved by the hosted API, so nothing is fetched or read locally and -L is refused with them. A local file named at a document or image input is uploaded first (a relative path in an inputs file resolves against that file's directory, and a file:// URI is read as its path), and the run's inputs carry its storage URI, while a .csv path at a structured-list input is not uploaded, so give its rows as a JSON list; an explicit null, such as an optional image left out, reaches the run as null. The method's signature is read only when some value of the inputs names an existing file on this machine, so inputs naming none go straight to the run, a method calling another one by its address included. Such a method cannot have a local file uploaded yet: give the file as an https:// URL. run bundle on a bundle file with no -L sends PIPELEXPATH's files too, as a local run loads them, and run method takes a method installed under a name spelled like a catalog id (mt_reports) as that method. --dry-run and --mock-inputs are refused on a hosted run, while --graph / --no-graph and --costs / --no-costs are accepted and have no effect there, since a hosted run writes nothing to disk and emits no cost events on this machine (pipelex run --hosted refuses --costs). Its requests name their client in the User-Agent as pipelex-cli/<version>, as pipelex run --hosted does, so the hosted API counts them as CLI runs.
The success envelope has the shape of a local run's. Compact, it is the main output's content. With --with-memory, main_stuff.json is that content (the hosted API renders no Markdown, so the markdown output shows the JSON), working_memory is the whole working memory, pipeline_run_id names the run on the hosted API, and uploads lists the files uploaded for it. A hosted run writes nothing to disk.
A run refuses an invalid bundle while loading it, before any pipe runs, with exit code 1 and the problem method.invalid, whose errors hold the items validate gives for that bundle, each with its code, its location (its pipe, its field) and its suggested fix when it has one. A refusal found while loading a library directory you pass with -L (or the method's folder, for run bundle <dir>) is reported the same way, its items naming their file in location.file; a bundle file run on its own is loaded from its text, so the items of a refusal found while loading it name no file, while a bundle that does not parse when run bundle looks for its main_pipe (no --pipe) names its file. In markdown, the items are listed one per line under ### Errors, as the JSON lists them. The items come from the verdict on the error's own chain of Pipelex errors: a failure of another kind raised while handling an earlier verdict, such as an unexpected error, lists none of that verdict's items.
Stdin inputs stay JSON
The agent CLI can also read inputs from stdin (a flat dict or a working_memory envelope). Stdin inputs are JSON-only — the extension-based TOML discrimination applies to --inputs file paths only. Like the main CLI, run bundle <dir> auto-detects inputs.json / inputs.toml when --inputs is omitted, erroring if both exist, and refuses one that is not a regular file, such as a symbolic link, which it never follows, with the problem request.invalid naming the field inputs; a file named with --inputs is read as given, a link followed. In a working_memory envelope each stuff must name its concept as the ref string; anything else there — the full concept object an older runtime dumped, for instance — is refused with the problem client.argument_invalid, its params.argument being stdin and its params.reason naming the member that does not hold the shape the contract asks for, as distinct from the reason not valid JSON, which means the JSON itself did not parse.
Validate
Validate pipes, bundles, or methods. Success output is markdown by default, or JSON with --format json. Errors follow --error-format (defaults to the program's --error-format, then to --format's value).
pipelex-agent validate pipe <PIPE_CODE> [OPTIONS]
pipelex-agent validate pipe --all [OPTIONS]
pipelex-agent validate bundle <PATH> [OPTIONS]
pipelex-agent validate method <NAME> [OPTIONS]
Common options:
--library-dir,-L- Additional library directory--allow-signatures- AcceptPipeSignatureplaceholders in the dependency graph (lenient mode)--format- Success output format:markdown(default) orjson--error-format- Error output format:markdownorjson(defaults to the program's--error-format, then to--format's value)
For bundle, additional options are available:
--pipe- Require a specific pipe in the bundle, and dry-run only that pipe; for a method's folder, the whole tree is still validated--graph,-g- Draw the method's flowchart from a dry run and save it in a new output folder kept out of the method's tree (see Graph Visualization)--graph-format,-f- Graph output format (mermaidflow,reactflow, orboth)--direction- Graph layout direction
validate bundle <folder> reads the folder as its method tree, the method rooted at the nearest METHODS.toml at or above it, else at the folder itself, and judges the whole tree, as the MTHDS protocol's validation judges a method: every bundle of it is loaded and every pipe of it dry-run. It needs no pipe to start at, so a folder whose bundles declare several main_pipes, or none, is validated like any other, and bundle_path in the envelope names the folder. Only the tree's own pipes are judged: the -L directories, a nested package below the folder with its own METHODS.toml, and the packages the method depends on all load for its bundles to use, but their own pipes are not judged. A graph, which is drawn from one pipe, is drawn from the pipe named by --pipe, else from the pipe a run of the folder starts at, the manifest's main_pipe or, with no manifest, the one main_pipe its bundles declare; when the folder decides no such pipe, --graph and --view draw nothing and the envelope carries graph_unavailable, a sentence saying why, while the validation stands and exits on its own verdict. A folder holding no .mthds bundle produces no verdict: the problem method.invalid, its diagnostics.error_class being EmptyMethodTreeError, with exit code 2. A configuration that does not validate, which the command reads before the boot for the tree's limits, produces none either: it is reported as the failed boot it would have been, under the configuration error's own code, with the boot's recovery tip in Markdown, and exit code 2, as pipelex validate bundle exits on it.
Signature pipes
pipelex-agent validate is strict by default — same as pipelex validate. A bundle whose dependency graph reaches a PipeSignature is rejected unless you pass --allow-signatures, which dry-runs signatures as mocks.
On a successful run, the envelope also carries pending_signatures — the library-wide list of pipes still declared as PipeSignature (unimplemented forward declarations), each namespaced by pipe_ref (domain.code). In JSON it is a pending_signatures array, in markdown a "Pending signatures" section. A top-down build reads it to see exactly which headers remain to implement. The envelope also carries a derived is_runnable boolean (true ⇔ pending_signatures is empty), and the markdown states the runnability verdict in plain English — runnable when complete, NOT yet runnable above the "Pending signatures" section otherwise. validate bundle, validate method, and validate pipe --all carry them and gate on them: without --allow-signatures, the command exits non-zero when is_runnable is false. Bare validate pipe <code> omits them, and a --pipe slice surfaces them for information without gating.
The files each step reads
validate bundle and validate method also carry tree_files_read and tree_file_readers, the members of the same name in the protocol's validation report (see Pipe Validate). tree_files_read maps each validated pipe's pipe_ref to the files of the method's tree it reads, each a {tree_path, use} object whose use is template, template_asset or python_module, and an empty list for a pipe reading none; tree_file_readers maps each of those files, by tree path, to the pipes that read it. A tree path is the file's path from the root of the method's tree, never a path on your disk. In markdown they render as a "Files the steps read" section listing each step that reads a file. validate pipe, which may span several trees, carries neither.
Every refusal of the bundle is an invalid verdict
A refusal of your input raised while the bundle is loaded or validated is answered with the invalid verdict — the problem method.invalid with is_valid: false, its faults in errors — and exit code 1, never a no-verdict failure and exit code 2, which is kept for failures of the tool or its environment. Each item carries its own code, the sentence that code renders as its detail, its params, its location (its pipe, its field, its file, and for a TOML syntax error its 1-based line and column) and, when there is one, its suggested_fix. A pipe whose dry run fails gives one item per failing pipe, coded by its failure and located at the innermost pipe that failed and is not allowed to fail (its file whenever the pipe is in your files; a validator of submitted content beside a host's library directories names only that content's files), with the sentence the failure's code renders for a caller, never the failure's own text, on validate bundle, validate pipe <code> and validate pipe --all alike; a controller that failed because a pipe it runs failed is reported once, at that pipe. Every error found while parsing is its own item, so a misspelled field is reported beside the categorized errors as an item of its own, located at its pipe and its file. An undeclared concept gives a method.concept_unresolved item whose params.concept names it. A pipe code or an input name that breaks the naming rules gives a method.identifier_invalid item whose params.name names it, and an item whose check knows facts its location does not hold, such as the concept a pipe declares and those that would fit, or the name two bundles both declare, gives them in its params, by the names the error registry gives them. A pipe naming a model the model deck does not define gives one model.unknown item located at the pipe's model field, its params naming the reference as written and its suggestions the deck's close matches, plus a rename-model suggested_fix when there is exactly one suggestion. That fix's safety is unsafe, because a close name can still be a different model: fix bundle never applies it, and the Markdown labels it unsafe, confirm before applying beside its fix code, where a safe fix is labelled safe. Any other refusal of your input with no code of its own gives one item with the refusal's code, located on its pipe when it was raised while that pipe was built. A pipe factory's refusal, such as a PipeExtract whose input is neither an image nor a document, is one of these items, located on its pipe. In Markdown the items are listed one per line under ### Errors, each with its code, its sentence, its location, its suggestions and its suggested fix. See Error Model.
Advisory warnings on validate
Whole-bundle and whole-library validate surfaces (validate bundle, validate method, validate pipe --all) also carry a warnings array — advisory findings on a VALID bundle that never flip the verdict or the exit code. Each entry has the same shape as a validation error item (category, error_type, pipe_code, domain_code, variable_names, message) — this is a different shape from the init/doctor setup warnings ({type, message}) documented under Output Contract below. Four families ride the array, always in this order: the useless-! lint (optional_force_redundant), a ! (force) input whose slot is guaranteed present in every analyzed flow, so the assertion can never fire; the vacuous-presence lint (input_presence_vacuous), an entry-pipe input that must be supplied but whose concept declares no required field, so the empty object satisfies it and a caller cannot tell what to fill in (see Understanding Optionality); the intent-hint lints (hint_unknown_key, hint_unknown_intent, hint_inapplicable_intent); and the warnings of a PipeDocGen step's template check (template_check_warning), found by the engine in the step's office template file during the dry run, as a dry_run item that also carries template_file (the file as the step names it) and template_location (the place in it, in the engine's words, absent when the engine names none). Every whole-bundle validate channel — this CLI, the bare CLI and the protocol validation report — assembles them from one composition point, so which advisories you see does not depend on which command you typed. Hint findings are bounded per site: a site naming many undefined keys reports the first few and then how many more there were, and a long authored key or value is elided in the message. In markdown, warnings render as a "Warnings" section. The array is empty when there is nothing to report. validate pipe <code> carries the template-check warnings alone: one pipe has no flow context to lint in, but what its dry run found is about that pipe. validate bundle/validate method with --pipe keep it, and it stays bundle-wide there — the slice narrows the dry run, not the validation.
Fix
Apply deterministic safe fixes to a bundle, re-validating after each round until the bundle is valid, no applicable fix remains, or the iteration limit is reached.
pipelex-agent fix bundle <PATH> [OPTIONS]
Options:
--library-dir,-L- Additional library directory; files in explicitly supplied directories may be fixed when an error identifies them as the source--allow-signatures- AcceptPipeSignatureplaceholders during validation--max-iterations- Limit the number of validate/apply rounds--select- Apply only the named fix rule; repeat for multiple rules--ignore- Skip the named fix rule; repeat for multiple rules (--selectand--ignoreare mutually exclusive)--format- Success output format:markdown(default) orjson--error-format- Error output format:markdownorjson(defaults to the program's--error-format, then to--format's value)
The result reports iterations, fixes_applied, files_written, and any remaining_errors. A fully valid result exits 0; a completed fix attempt that remains invalid exits 1 with the problem method.invalid on stderr, its errors the faults left, each with the code validate bundle gives it, and beside them is_valid, bundle_path, iterations, fixes_applied, files_written and any bail_reason; argument, setup, or unexpected failures exit 2, and so does a folder holding no .mthds bundle. A fix the command cannot write, its round rolled back, is client.write_failed, whose params.path names the file, or the directory a copy staged beside it could not be written in, and params.reason the system's reason, never client.file_unreadable, which is a method file it cannot read. A folder is fixed whole, as validate bundle judges it: every file of its method tree, from the tree's root down, is inside the write scope, a fix lands in the file its error names, and a folder whose bundles declare several main_pipes, or none, is fixed like any other. A folder whose tree cannot be read, such as one past this machine's limits, leaves as the still-invalid problem method.invalid with exit code 1, its errors carrying the refusal. A configuration that does not validate, which the command reads before the boot for the tree's limits, is reported as the failed boot it would have been, under the configuration error's own code, with the boot's recovery tip in Markdown, and exits 2, since it gives no verdict, as pipelex fix bundle exits on it.
Rewrite a method's flat PipeFunc function names as import paths from its folder, the migration of the retired function registry (see PipeFunc):
pipelex-agent fix function-names <PATH> [--dry-run] [--format markdown|json] [--error-format markdown|json]
The result reports directory, is_dry_run, changed_files, the rewrites it made (each with its bundle, pipe_code, the flat_name and the import_path), the marker_removals (each @pipe_func(name=...) marker whose argument it dropped), the unresolved flat names it left, each with the reason and, when several modules register the name, their import paths in candidates, and the unresolved_markers whose argument it left in place, each with its module, function, registered_name and reason: a marker registering a name some step still holds unresolved, or one whose name is not a string literal. Branch on is_complete, which is true when no flat name and no marker was left unresolved. Nothing is imported and Pipelex does not boot. The command exits 1 when a flat name or a marker was left, and 2 when the folder cannot be listed as a method tree or a file cannot be read or written.
Inputs
Generate an example inputs template for a pipe, bundle, or method. By default the template is the light signature-driven shape (bare values matching the pipe's declared concepts), which is exactly what run accepts; --explicit emits the ceremonial {concept, content} envelope form instead.
pipelex-agent inputs pipe <PIPE_CODE> [OPTIONS]
pipelex-agent inputs bundle <PATH> [OPTIONS]
pipelex-agent inputs method <NAME> [OPTIONS]
Common options:
--library-dir,-L- Additional library directory--format- Template serialization:json(default) ortoml--explicit- Emit the ceremonial{concept, content}envelope form instead of the light values--error-format- Error output format:markdownorjson(defaults to the program's--error-format, then tojson; never to--format, which here selects the template's serialization)
For bundle and method, use --pipe to target a specific pipe. With no --pipe, a method's folder generates the template of the pipe a run of the folder starts at, as run bundle selects it; a folder whose bundles declare no main_pipe, or several, is refused with the problem request.pipe_unspecified and exit code 1, its diagnostics.error_class being MethodEntryError, and a folder holding no bundle with the problem method.invalid, exit code 1 too. With --pipe, the folder loads as the method's library and the pipe is looked up there, as run bundle <folder> --pipe runs it.
The JSON success envelope names the pipe the template was generated for as pipe_ref, the qualified domain.pipe_code of the pipe that was resolved, whether you named it with a bare code, a qualified ref, or let it default to the bundle's main_pipe. A pipe you reached through a dependency alias keeps that alias, alias->domain.pipe_code, since that is the ref that selects it again:
{
"success": true,
"pipe_ref": "my_domain.main_pipe",
"inputs": { ... }
}
A pipe that declares no inputs is not an error: the envelope carries "inputs": {} and the command exits 0; under --format toml it prints the comment # Pipe 'my_domain.main_pipe' declares no inputs.
inputs --format is json|toml, not markdown|json
Unlike run/validate, the inputs command's --format selects the template serialization, not a presentation style. json (the default) emits the structured JSON success envelope; toml prints the raw TOML template straight to stdout (a pipe with no inputs prints a TOML comment line, which loads back as an empty dict). Its errors follow its own --error-format, else the program's, else JSON, never its --format.
--explicit and concept hints
The light --format toml template carries the declared concept for each key as a # concept: ... comment; the light --format json template (the default) cannot (JSON has no comments), so pass --explicit when you want the concept written out inline. The JSON success envelope shape (success / pipe_ref / inputs) is unchanged — only the inputs payload flips between the light values and the envelope form.
Flat Commands
These commands do not have subcommands:
| Command | Description |
|---|---|
init |
Initialize configuration non-interactively from --config: execution (hosted or local), backends, primary_backend; see Non-Interactive Init (--format markdown\|json success, default: markdown; --error-format for errors, defaults to the program's --error-format, then to --format's value) |
fmt |
Format a .mthds/.toml/.plx file in-place (delegates to plxt) |
lint |
Lint a .mthds/.toml/.plx file for errors (delegates to plxt) |
models |
List available model presets, aliases, and waterfalls (--format markdown\|json success, default: markdown; --error-format for errors, defaults to the program's --error-format, then to --format's value) |
check-model |
Check a model reference for a model category (check-model <reference> --type <category>, the category being llm, extract, img_gen, search, judgment or doc_gen) and suggest alternatives when it is not valid. It applies the rule a validation applies, as GET /v1/models/check does: a sigiled reference names that kind of binding, and a bare name is valid when it is a model of the category, an alias of the category, or a waterfall of the category while model fallback is on; a handle: reference writes a bare name without reading a sigil, so handle:@best-gpt is the bare name @best-gpt, not the alias best-gpt. --format markdown\|json success, default: markdown; --error-format for errors, defaults to the program's --error-format, then to --format's value |
doctor |
Check config, credentials, and model health; a hosted setup ([run] execution = "hosted") is judged by its Pipelex API key, reported in checks.pipelex_api_key, with execution at the top level and the provider credentials and models rows marked informational (--format markdown\|json success, default: markdown; --error-format for errors, defaults to the program's --error-format, then to --format's value) |
Output Contract
Commands use different stdout formats depending on their purpose:
- Markdown or JSON:
run,validate,fix,init,models,check-model,doctor,migrate,codegen types,codegen check— markdown by default, JSON with--format json. Error format follows--error-format(defaults to--format's value, so--format jsonflips both). - JSON or raw TOML:
inputs— structured JSON viaagent_success()by default (--format json), or the raw TOML template printed directly to stdout with--format toml - Passthrough:
fmt,lint— rawplxtoutput
JSON success — written to stdout:
{
"success": true,
"target_dir": "/path/to/.pipelex",
"backends_enabled": ["openai"]
}
JSON commands return the result object directly. They are not wrapped in a status or data envelope.
Error — written to stderr, one problem per failure. In JSON it is the problem document, the error document of the MTHDS standard that the hosted API answers too, in the operator's view, since whoever runs this CLI runs the runtime, except for a run on a runner, whose failure is the runner's or the client's problem in the caller's view (see below):
{
"type": "https://mthds.ai/errors/client.file_unreadable",
"title": "A local file cannot be read",
"detail": "The local file '/path/to/bundle.mthds' cannot be read (reason: not found).",
"code": "client.file_unreadable",
"domain": "input",
"retry": "no",
"next_step": {
"kind": "change_input",
"detail": "Check that '/path/to/bundle.mthds' exists and is readable, then try again."
},
"params": {"path": "/path/to/bundle.mthds", "reason": "not found"},
"diagnostics": {"error_class": "FileNotFoundError", "message": "[Errno 2] No such file or directory: '/path/to/bundle.mthds'", "runtime_version": "0.87.0"},
"message": "Bundle file not found: /path/to/bundle.mthds"
}
For the structured commands, exactly one problem is written to stderr per failure, so the stream parses as a single JSON document.
Branch on code, the stable name of the case in the error registry, and on what it decides: domain, who acts on the failure (input, method, account, operator or runtime), retry, whether sending the same thing again can help (yes, no, or outcome_unknown when a run may have started and should be looked up rather than started again), and next_step, whose kind an agent can branch on and whose detail says what to do. params holds the code's parameters, location where the fault is (the pipe and its path from the entry pipe, the input, the field, the file, the line), errors the faults one by one when there are several, each with its own code, detail, params, location, suggestions and suggested_fix, request_id and pipeline_run_id what to quote when there is a request or a run behind it, and diagnostics the operator's facts: the class of the error behind the failure, its message capped, its causes, the model and the provider of a failed inference call, and a pending configuration migration. Every member that has nothing to say is left out. Beside the problem, message is the command's own sentence about the failure, and a command adds its own members, such as is_valid on an invalid verdict. A refusal the command makes itself has a code of its own: client.argument_invalid for an argument (params.argument and params.reason), client.file_unreadable for a file it cannot read (params.path and params.reason, which is not found or the system's own reason, such as permission denied or is a directory, for the bundle file of validate bundle, inputs bundle, fix bundle and run bundle, and not UTF-8 text too for an --inputs file and init --config's file), client.tool_missing when plxt is not installed, and pipelex.config.file_missing when init finds a configuration file it should have written missing. An error of the system that names no file, such as a refused connection or a timeout met while a bundle command runs, refuses no file: it is runtime.unexpected. A bundle file that is not UTF-8 text does not parse, so every bundle command reports it as validate bundle does: the invalid verdict, its item method.syntax, located at the first byte that does not decode. A failure that is no Pipelex error, which is a bug, is runtime.unexpected.
In Markdown, the default, the problem reads as a ## <title> heading, the detail, the command's own sentence when it says more, the **Next step** with its kind, the advice the error gives as a 💡 tip when it says more than the next step (a boot that fails with no advice of its own tips the recovery path: pipelex-agent doctor, then pipelex-agent migrate --dry-run --format json, shown to the user, and --yes, or pipelex-agent init, --global for the home configuration, which writes a fresh configuration and discards its settings, while the problem, its code and diagnostics.error_class included, stays the failure's own), then the code with its domain and retry verdict, the request, the run and the location, the faults under ### Errors, one per line with the item's code, its sentence and its location, then its suggestions and its suggested fix (an unsafe fix says to confirm it before applying it), the same items the JSON's errors lists, a ### Diagnostics section and the command's own members under ### Details. No stack frame is printed in either format.
A failed run (run pipe, run bundle, run method) reports the problem of the root fault, the innermost Pipelex error behind the failure, located at the pipe that failed: location.pipe names it and location.path its path from the entry pipe. The message is that root fault's message prefixed with the failing pipe and its path (Pipe 'summarize' failed (two_steps → summarize): …), and the code, the domain, the retry verdict and the next step are the root fault's, so branch on code to tell, say, a model that is not available from a failure to combine a parallel's results. A failed inference call carries its model and its provider in diagnostics.model and diagnostics.provider.
A run on a runner (--runner hosted, on the hosted Pipelex API or on the runner --base-url names) that fails reports the problem the runner answered or the client decided, as it came, in the caller's view, since the runner is someone else's runtime: no diagnostics, and the next step the runner gave its caller. It is Markdown by default and JSON with --error-format json, like any other failure. What this CLI refuses before anything is sent, a --runner that contradicts --hosted, a --base-url on a local run, a local-only flag, a configuration that cannot be read, is a problem like any local refusal, in the operator's view. The method itself is the exception: run bundle reads its bundle before it sends it, and a bundle that does not parse or does not validate (method.invalid, its faults in errors, each with its file and its place in it), or a bundle file or folder with no pipe to start at (request.pipe_unspecified, its params.candidates the pipes it could start at), is refused then, in the caller's view, as the runner would refuse the same method: no diagnostics and no message, so no class name and none of the text only an operator reads, while the code, the items, their locations and the next step stay. A run on this machine reports the same refusal in the operator's view, with the command's own sentence. The failures of the run itself read as follows.
- A run the runner refuses reports the runner's problem document by the members the standard and Pipelex declare: its
code,domain,retry,next_step,params,location, itserrorsitems, each with itscode,detail,params,location,suggestionsandsuggested_fix, itsstatusand itsrequest_id, which is what to quote when the fault is the runner's. A member no reader declares, at any level, is left out, and so is a member of Pipelex's (request_id,run_error) in a shape this version does not read, alone, so the problem around it is still reported. An answer that carriesdiagnostics, which a runner answers only to its operator, is projected onto the caller's view, its next step re-aimed at the caller, and so is the run's problem an answer nests asrun_error, which is the run's stored record, whatever view the answer around it is in. A run that failed at a pipe is located there,location.pipeandlocation.path. A method refused before any pipe ran ismethod.invalid, its faults inerrors. - An answer that is no problem document, the bare answer of the hosted API's edge, a gateway or a proxy in front of the runner, is read from its status by pipelex-sdk, and this CLI reads the refusals of a credential and of a route itself, with the next step
pipelex run --hostedgives. A bare401or403isauth.missing_credentialwhen the request carried no key, telling how to get one withpipelex login, andauth.invalid_credentialwhen it carried one, telling to runpipelex loginor correctPIPELEX_API_KEY, a403adding that a valid key may also be refused a route it may not call. A bare404isrequest.not_found, telling to check the origin,--base-url, elsePIPELEX_BASE_URL. Every other bare answer keeps pipelex-sdk's reading:runtime.unavailablewithretry: yesfor a408, a429, a502, a503or a504, with the delay itsRetry-Afterheader states asretry_after,runtime.unexpectedfor a500, andclient.response_unreadablenaming the status for any other. A server fault on the run request, a bare408or any bare5xxbut a501answered to the start or the execute request, isclient.outcome_unknown, because the runner may have started the run before it failed to answer: check the run history before running again, since running again can repeat a paid run. A problem document the runner answered is reported as it came, whatever its status, so a runner's own fault on the run request keeps its own code. - A runner that cannot be reached is
client.unreachable, naming the base URL inparams.base_url, orclient.timeout, naming the time limit, for a request whose time limit ran out, both withretry: yes: check the network connection and the origin,--base-url, elsePIPELEX_BASE_URL, then run again. A connection lost on the start or the execute request, unless it proves that request never left, isclient.outcome_unknown, since a run may exist with no id returned; one lost on a request sent before the run's, such as the check of which files of a large tree the runner holds, reads as a runner that cannot be reached. - A file of the method's tree that could not be uploaded, a tree past 256 KiB travelling by the hash of each file, stops the run before anything starts. Its problem is pipelex-sdk's reading of why the upload failed:
runtime.unavailablewithretry: yesand aretrystep to run again when the storage could not be reached, timed out, throttled the upload or failed, andclient.response_unreadablenaming the status withretry: noand areportstep when the storage refused it.messagenames the file's SHA-256, never the upload link. - A run that started and failed is
run.failed, naming the run and the status it ended with inparamsandpipeline_run_id, andmessageis the client's sentence about it. The run's own problem rides asrun_error, read as a runner's answer is, by its declared members alone, and in the caller's view, since it is the run's stored record: its code, its next step and its items say why the run did not succeed. Markdown renders it under### The run's own problemas a problem is, one heading level down, which is whatpipelex run --hostedtells of the same run. - A run that failed on the way carries the code of what happened:
client.follow_failedfor a run that keeps going past the wait, or that started and whose result could not be read (it may still be running and is paid for, so look it up rather than run again),client.response_unreadablefor a completed run that delivered no main output,method.invalidfor a method the hosted API found does not load before any run starts, when the inputs name a local file (itserrorsthe items the hosted API's verdict lists, as they came, by their declared members, so an item under a code newer than this CLI keeps the runner's sentence and parameters),request.unsupportedwithparams.reasonlocal_file_with_address_dependencywhen the inputs name a local file and the method calls another one by its address (give the file as anhttps://URL), andclient.not_configuredfor a base URL that is not an origin, refused before anything is sent. Every failure met once the hosted API acknowledged the run carries itspipeline_run_id. pipelex's own failures add their sentence asmessageand their advice as the Markdown's💡tip, and so do the client's own, whose sentence says what the code's parameters cannot, such as how long it waited for a run that did not finish. A failure the client meets on its own side takes the next steppipelex run --hostedtailors for it, in place of its code's: where to find a run that did not finish, that a blocking run cut off may still be going, that the server takes no uploads, which file path to check. - An interrupted run (Ctrl-C) exits with
130:client.follow_failednaming the run the hosted API had acknowledged, which keeps going and is paid for, orclient.outcome_unknownwith nopipeline_run_idwhen it was interrupted before the acknowledgement, so check the run history before running again. - Any other failure of the run is this process's own and is reported as a local one is, in the operator's view: a Pipelex error under its own code, any other exception, such as a results body that drifted, as
runtime.unexpected.
None of the members of the envelope this CLI wrote before it reported problems is left on this path, at any level of the problem: no error, error_type, error_source, hint, error_domain, retryable, error_code or http_status.
A pending configuration migration — a configuration error (pipelex.config.invalid, "domain": "operator") carries diagnostics.migration when, and only when, a scan of this machine's configuration directories found something. Its presence says the migration history has something to report about these files; would_write says whether a command repairs them. The object holds remedy (the command that fixes what can be fixed), would_write (whether running it would rewrite any of these files), needs_attention (whether something there is a person's decision rather than the tool's), and plans — one per file, in the same shape pipelex-agent migrate emits under its own plans key. On would_write: true the loop is pipelex-agent migrate --dry-run --format json, show the user what would change, then --yes. On would_write: false there is nothing to run: the plans carry a path no ledger entry explains, or an entry blocked before anything applied, and the command would visit the files, write nothing and leave the same error standing — so read the plans and correct what they name. Never answer either one by re-initializing a configuration file — that discards every setting the migration would have kept. In Markdown it is a ### Configuration migration section under the diagnostics.
A telemetry configuration that does not load (diagnostics.error_class TelemetryConfigValidationError) carries it too, on the same terms: absent means the telemetry.toml is wrong and the fields the message names are what to correct, present with would_write: true means it is old, and present with would_write: false means it is both reported on and unrepairable by the command.
A configuration that does not validate names, in the next step's sentence, the setting at fault, its file and the configuration it belongs to (params.setting, params.file and params.surface, which only the operator's view carries): Correct the setting 'runtime.log.level' in the file 'pipelex.toml' of the pipelex configuration, then restart the runtime. A refusal of the merged layers names the configuration's file, pipelex.toml or telemetry.toml, since the setting can come from any of them, and a refusal that knows the one file at fault names that file: a file that does not parse, with its line and column, or a file a former release left. Such a file is named relative to the working directory when it is under it, else as ~/… when it is under the home directory, so a step never names the home directory's account. The Markdown tip reads the migration: with would_write: true, the migrate loop above; with would_write: false, correct by hand what the plans name; with none, the code: a missing file (pipelex.config.file_missing) is restored or written by pipelex-agent init, a variable that is not set (pipelex.config.variable_missing) is set where the configuration reads it, and a configuration that does not validate or parse is what pipelex-agent doctor finds.
A configuration a former release left — a boot on a configuration set up by a release that ran on the Pipelex Gateway, with its backend enabled or one of its routing profiles active, is refused with pipelex.config.invalid (diagnostics.error_class FormerReleaseConfigError), whose message names what stops it with its file, and whose diagnostics.migration says what pipelex migrate would do about it, read off the cleanup's own dry run: would_write whether it would rewrite or remove a file, needs_attention whether something is left for a person, a file it would leave for a hand edit, a boot it would still not start or a file outside the directories it walks, and plans empty, since the cleanup's files are the command's former_release key. A JSON reader branches on it as on any pending migration. The Markdown tip gives the loop, the same one: pipelex-agent migrate --dry-run --format json, whose former_release key lists each file the cleanup rewrites or removes and its changes, show the user, then --yes. On every run, former_release.still_blocking lists, one sentence each, whatever would still stop the boot once the cleanup is done: after --yes, read off the files written, and in a dry run, off the files as they would be written, so the plan already says when the cleanup alone will not be enough. When it is not empty, needs_attention is true and is_clean false, even with no file left to clean, and the machine is not fixed yet.
A failure is not the only way to learn this, and for an agent it is not even the reliable one. A configuration the migration history explains boots with a warning rather than an error — and pipelex-agent silences logging process-wide before anything can emit one, so no error arrives and the machine is still stale. pipelex-agent doctor is the channel that always answers: its checks.pending_migrations row carries a finding (up_to_date, pending, needs_attention, unavailable), the migratable_files a migration would rewrite, the attention_files it will not repair on its own, and the former_release_files its first step cleans up, with former_release_blocks_boot saying whether they stop this machine's boot, read off the files the boot merges in the current directory rather than off each directory alone. When the cleanup's own check finds that a boot would still not start once it has run, the row says so in its message and recommended_actions names pipelex migrate --dry-run, which says why; with nothing left to clean its finding is needs_attention, and that action is the row's only one. Branch on finding; unavailable means the check could not run and is not a claim that the machine is current. The row answers for both configuration directories even under --global, because it describes pipelex migrate, which has no such flag.
fmt and lint are raw passthroughs to the plxt binary and bypass the JSON output contract, producing native plxt output instead.
Usage Statistics
pipelex-agent sends Pipelex's anonymous usage statistics as pipelex does: one command_completed per command, plus the events of the runs, validations and code generations it performs, all off under DO_NOT_TRACK=1, PIPELEX_USAGE_STATS=0 or pipelex usage-stats disable. When the invocation's errors are JSON, with the command's --error-format json, the program's --error-format json given before the command, or --format json and no --error-format at all, stderr is a machine contract, so the one-time notice is neither printed nor recorded there; the next command whose stderr a person reads prints it. A command's command_completed names its failure's code and domain as its problem gives them, and a failed hosted run's names what the hosted API sent: the problem document's code and domain when it sent both, else nothing, never a domain the client inferred from the status. The plxt that fmt and lint start reports nothing of its own. There is no usage-stats group on this CLI: pipelex usage-stats shows and changes the statistics for both.
Graph Visualization
Graph visualization is available through the validate bundle subcommand with the --graph flag, and through run subcommands where it is enabled by default. Use --no-graph on run to disable it.
validate bundle --graph writes its flowchart, the ReactFlow viewer dry_run.html, and the Mermaid viewer mermaidflow.html with --graph-format mermaidflow or both, into a new numbered folder, <pipe_code>_output_<NN>/, under the results/ directory a run bundle of the same target writes its outputs to: beside the bundle file named, beside the bundle defining a folder's entry pipe, or in the folder's own results/ when --pipe names the pipe, whichever bundle defines it. The folder is made the way a run's is, holding a .gitignore ignoring everything in it, so validating a method never adds a file to its tree nor changes its digest, and each validation gets a folder of its own. The envelope names each file it wrote in graph_files (reactflow_html, mermaidflow_html) and the folder in graph_output_dir, and the Markdown lists the files under ## Graph files: open the path the command reports rather than looking for the viewer in a fixed place.
A run writes its outputs into a new numbered folder, results/<pipe_code>_output_<NN>/ beside its bundle, or under mthds-wip/ when it ran no bundle file; a folder run that named its pipe writes under the folder's results/, and a fetched method under results/ in the working directory. The folder holds a .gitignore ignoring everything in it, so no output ever joins the method tree and the next run reads the same tree, with the same method_tree_digest. With the graph on, that folder holds the ReactFlow viewer (dry_run.html or live_run.html) and the graph itself: graphspec.json on a dry run, live_run_graph.json on a live run. Beside the graph, under their canonical names whatever the graph file is called, sit the three files that describe its data: pipe_io_contracts.json, input_form.json and output_form.json, the same artifacts validate reports under those names, keyed by namespaced pipe_ref. A graph viewer resolves them from the graph file's directory, not from its name. With --with-memory, the JSON envelope's graph_files object names every one of these paths (graph_html, graph_spec on a live run, pipe_io_contracts, input_form, output_form).