CLI I/O Contract
This page defines the input/output contract for MTHDS methods when invoked as CLI commands. It specifies what a method writes to stdout, what it reads from stdin, and how errors propagate through pipe chains.
Stuffs on the Wire
A stuff — one named value in a working memory, whether a method input or a pipe's result — crosses every boundary on this page in one shape:
{
"concept": "legal.ContractAnalysis",
"content": { "clauses": ["..."], "overall_risk": "high" }
}
concept names the concept by its crate key, the string the library crate keys it under: the domain-qualified reference <domain>.<Code> for a concept the method's own package declares (legal.ContractAnalysis) or a native concept (native.Text), and the address-prefixed <package_address>::<domain>.<Code> for a concept a dependency contributes (github.com/mthds/scoring-lib::scoring.ScoreResult). Two packages may declare the same domain and code, so a dependency's concept is never named by the bare <domain>.<Code>, and the prefix is the package address, never the alias one consumer happens to give it. content is the value, shaped as the concept's structure dictates. The same string names the concept wherever a stuff travels: an entry of --inputs, a stuff read from stdin, every entry of working_memory.root in the --with-memory envelope, every stuff of a runner's pipe_output over the HTTP protocol, and any file a runtime writes from a working memory.
The reference is the whole of what a stuff says about its concept. The concept's definition — its description, its structure, what it refines, and anything an implementation attaches to it, such as the name of a runtime class — belongs to the library the method loads and never travels beside a stuff. A consumer that needs the definition resolves the reference against that library, or reads it from the pipe I/O contracts a validating runtime reports. A runtime MUST emit concept as this string and MUST NOT emit an object in its place.
On input, a runtime MUST accept the crate key. As a convenience for a caller typing inputs by hand, it MAY also accept a package-qualified reference (alias->domain.Code) or a bare concept code and resolve either against the method's library — how it resolves an ambiguous bare code is implementation-defined — and it MAY accept a bare value in place of the stuff (a string, a number, an object or a list) and shape it against the input's declared concept. What it emits is always a stuff, its concept the crate key.
An implementation MAY carry fields of its own beside concept and content — an identifier for the stuff, for instance. A consumer ignores fields it does not know and never needs them to read the value.
Output Modes
A method's CLI produces structured JSON on stdout. Two output modes are defined: compact (default) and full (opt-in via --with-memory).
Compact Output (Default)
The concept's rendered JSON is emitted directly — no envelope, no metadata:
{
"clauses": [
{ "title": "Non-Compete", "risk_level": "high" },
{ "title": "Termination", "risk_level": "medium" }
],
"overall_risk": "high"
}
This is the structured content of the method's main output concept. Standard JSON tools work directly:
mthds-agent pipelex run method extract-terms | jq '.clauses[] | select(.risk_level == "high")'
# Or using the installed CLI shim:
extract-terms | jq '.clauses[] | select(.risk_level == "high")'
A completed method always has a main output. If the declared output resolves as absent (for example, an optional ? output produced no value), the CLI emits an explicit absence document rather than an empty or missing result.
Full Output (--with-memory)
When --with-memory is passed, the output includes the main stuff renderings and the full working memory:
{
"main_stuff": {
"json": "<concept as JSON string>",
"markdown": "<concept as Markdown string>",
"html": "<concept as HTML string>"
},
"working_memory": {
"root": {
"contract_text": {
"concept": "native.Text",
"content": { "text": "The parties agree..." }
},
"extracted_terms": {
"concept": "legal.ContractAnalysis",
"content": { "clauses": ["..."], "overall_risk": "high" }
},
"main_stuff": {
"concept": "legal.ContractAnalysis",
"content": { "clauses": ["..."], "overall_risk": "high" }
}
},
"aliases": {
"main_stuff": "extracted_terms"
}
}
}
Every entry of root is a stuff in the wire form: its concept is the concept's crate key, never the concept's definition. The full output preserves all intermediate results and aliases from the pipeline's working memory. This is required when piping output to another method, because the downstream method may need intermediate stuffs for multi-input binding.
Side Effects
Regardless of output mode, the runtime may write side-effect files to disk:
- Output JSON (
live_run.json/dry_run.json) — the full execution result saved alongside the bundle. - Graph HTML (
live_run.html/dry_run.html) — execution graph visualizations (generated by default, disabled with--no-graph).
These files are not included in stdout output. Their paths appear in runtime logs on stderr.
Input Acceptance
A method accepts inputs through three sources, resolved in priority order:
1. --inputs Flag (Highest Priority)
The --inputs / -i flag accepts a file path or inline JSON string. If the value starts with {, it is parsed as inline JSON; otherwise it is treated as a file path.
# Inline JSON
mthds-agent pipelex run method my_method --inputs '{"text": {"concept": "native.Text", "content": {"text": "hello"}}}'
# File path
mthds-agent pipelex run method my_method --inputs data.json
When a method is installed as a CLI shim (see CLI Reference), the same commands are available as:
my_method --inputs '{"text": {"concept": "native.Text", "content": {"text": "hello"}}}'
my_method --inputs data.json
When --inputs is provided, stdin is ignored entirely. This allows overriding piped data for debugging.
2. stdin (Fallback)
When --inputs is not provided and stdin is not a TTY (i.e., data is being piped), JSON is read from stdin:
echo '{"text": {"concept": "native.Text", "content": {"text": "hello"}}}' | mthds-agent pipelex run method my_method
When stdin is a TTY (interactive terminal), no stdin reading occurs and the method runs without inputs.
3. Auto-detected inputs.json
In directory mode, inputs.json in the target directory is auto-detected and used as a fallback when no explicit inputs are provided. The membership rule keeps a file named inputs.json out of the method tree wherever it is, whether the target directory is the method's root or a directory below it, so reading it as the run's inputs never carries it into the method's files. Auto-detection matches the entry name inputs.json byte for byte, as the membership rule does, so a tool looks the name up among the directory's entries rather than opening it: on a case-insensitive file system, opening inputs.json would also find a case variant such as Inputs.json, which is a member of the tree and is never read as the run's inputs.
Envelope Detection
When JSON arrives via stdin, the runtime distinguishes between two formats based on the presence of a working_memory key at the top level:
Flat Inputs
No working_memory key present. The JSON is treated as direct input bindings — the same format as --inputs, each binding a stuff in the wire form:
{
"document": {
"concept": "native.Document",
"content": { "url": "/path/to/file.pdf" }
}
}
Full Envelope
A working_memory key is present. This indicates the JSON came from an upstream method's --with-memory output. The runtime extracts named stuffs from working_memory.root and converts them to input bindings.
The input resolution rules when receiving a full envelope:
-
Name matching: Each stuff name in the upstream working memory's
rootis matched against the downstream method's declared input names. Matching entries are bound automatically. -
Single-input shortcut: If the downstream method declares exactly one input, it auto-binds to the upstream's
main_stuffcontent. This is the common case for simple chains:extract-terms --with-memory | assess-risk -
Error on failure: If the downstream method's declared inputs cannot be satisfied from the upstream's working memory, the downstream method fails with an
input.missingproblem naming the input it cannot satisfy, as Error Propagation shows. When it cannot satisfy several, it takes them in authored input order, the order in which the method declares its inputs and in which the input-form descriptor lists them: the problem's ownparamsandlocationname the first of them, and itserrorscarries one item per input it cannot satisfy, in that same order, each naming its input in its ownparamsandlocation.
Error Propagation
When a method invoked from the command line fails, its CLI writes the error on stderr and exits with a non-zero exit code. The error is the problem document the HTTP protocol defines under Errors, without its status member: otherwise the same members with the same meanings, codes from the same registries, and the same rules for whoever emits or reads it. It is the problem document of the protocol version that the release of the standard the CLI implements names, as Versioning states. In its JSON form it is one object:
{
"type": "https://mthds.ai/errors/input.missing",
"title": "A required input is missing",
"detail": "The method requires the input 'analysis', and no stuff of that name was provided.",
"code": "input.missing",
"domain": "input",
"retry": "no",
"next_step": {
"kind": "change_input",
"detail": "Provide a stuff named 'analysis', with --inputs or on stdin, then run the method again."
},
"params": { "input": "analysis" },
"location": { "input": "analysis" }
}
This is the problem a method in a chain writes when it declares several inputs, among them an input named analysis that it cannot run without, and the full envelope it reads holds no stuff of that name. code says what happened. domain says who must act, here the caller, by changing the inputs. retry says whether running the method again with the same inputs can succeed, here no. next_step says what to do next, as a kind from the protocol's closed list and a sentence. params holds the facts the sentences were written from, and location says where the error happened, here the input it concerns. When an error happens inside a pipe, location.pipe names the pipe that failed rather than a controller around it, and location.path lists the pipe codes from the method's main pipe down to it, as Parameters and location defines.
The other members appear under the protocol's own conditions: retry_after when the wait is known, beside a retry of yes; errors, one item per fault, when the problem has several, such as a method that does not validate; and pipeline_run_id when a run exists. An implementation MAY add members of its own, and a reader ignores the members it does not know, by the protocol's rules for readers and emitters.
A CLI's problem carries no status. That member is the HTTP status of the response a problem is the body of, and the protocol leaves it out of every problem kept anywhere else. A CLI MUST leave it out of the problems it writes, including a problem it relays from a runner it called over HTTP. The non-zero exit code says only that the method did not complete: a program reading the error branches on code, and on domain, retry and next_step.kind when it handles several codes alike, never on the exit code, and it never parses title, detail or next_step.detail, which are written for a person.
A rendering is presentation. A CLI read by a person or an agent MAY write a rendering of the problem on stderr in place of its JSON form, such as Markdown, stating at least the problem's title, its detail, its next step and its code. The --error-format option chooses the form: --error-format json asks for the JSON form, and --error-format markdown asks a CLI that writes a Markdown rendering for that rendering. Every CLI that runs a method MUST accept --error-format json and answer it with the JSON form, a CLI that only ever writes the JSON form included, so that a program passes it to any CLI without knowing which kind it calls and never reads a rendering. The option governs what is written on stderr when the method fails and leaves what is written on stdout to Output Modes.
In a Unix pipe chain, errors stop execution at the failing step. Use set -o pipefail in shell scripts to ensure mid-chain failures propagate:
set -o pipefail
extract-terms --with-memory \
| assess-risk --with-memory \
| generate-report
Examples
Simple Chain (Single Input)
# Extract terms, then assess risk, then generate report
extract-terms --inputs '{"document": {"concept": "native.Document", "content": {"url": "contract.pdf"}}}' --with-memory \
| assess-risk --with-memory \
| generate-report
Each intermediate step uses --with-memory to pass the full envelope. The final step omits it to produce compact output.
Compact Output with jq
# Extract just the high-risk clauses
extract-terms --inputs data.json \
| jq '.clauses[] | select(.risk_level == "high")'
Override Piped Input
# The --inputs flag overrides whatever comes from stdin
echo '{"text": {"concept": "native.Text", "content": {"text": "from stdin"}}}' \
| mthds-agent pipelex run method my_method --inputs '{"text": {"concept": "native.Text", "content": {"text": "from the flag"}}}'
The --inputs flag always wins — the stdin data is ignored.