What you build
These are the calls you write inside your command handlers. murli provides the output methods; you call them with your data.
WriteSuccess
Call at the end of a successful handler. At a TTY it prints the human text; when piped or in agent mode it writes the JSON success envelope with the payload as the result field.
results := []map[string]any{
{"path": "/docs/woodworking", "score": 0.95},
}
w.WriteSuccess("Found 1 result", results)
let results = json!([{"path": "/docs/woodworking", "score": 0.95}]);
writer.write_success("Found 1 result", &results);
results = [{"path": "/docs/woodworking", "score": 0.95}]
writer.write_success("Found 1 result", results)
JSON output (all languages):
{
"status": "ok",
"schema_version": "1.0",
"tool_version": "1.0.2",
"message": "Found 1 result",
"result": [{ "path": "/docs/woodworking", "score": 0.95 }]
}
WriteError
Writes the error envelope to stderr and exits with the code from the error. Do not return from the handler after calling WriteError (Go/Rust) or raising AgentError (Python).
if !indexExists {
w.WriteError(murli.NewUserError(
"index not found",
"Run `mytool index build` to create the index.",
))
// execution stops here — WriteError calls os.Exit(1)
}
if err != nil {
w.WriteError(murli.NewToolError(err.Error()))
}
if !index_exists {
writer.write_error(AgentError::not_found(
"index not found",
"Run `mytool index build` to create the index.",
));
// write_error calls process::exit — execution stops here
}
if let Err(e) = result {
writer.write_error(AgentError::tool_error(&e.to_string()));
}
if not index_exists:
raise murli.AgentError.not_found(
"index not found",
"Run `mytool index build` to create the index.",
)
# For internal errors:
raise murli.AgentError.tool_error(str(err))
WriteEvent — NDJSON streaming
Use for commands that produce output incrementally. Each call writes one JSON object on one line to stdout.
for _, chunk := range chunks {
w.WriteEvent(map[string]any{"event": "chunk", "index": chunk.Index, "data": chunk.Text})
}
w.WriteSuccess("Processed all chunks", summary)
for chunk in &chunks {
writer.write_event(&json!({"event": "chunk", "index": chunk.index, "data": chunk.text}));
}
writer.write_success("Processed all chunks", &json!(summary));
for chunk in chunks:
writer.write_event({"event": "chunk", "index": chunk.index, "data": chunk.text})
writer.write_success("Processed all chunks", summary)
{"event":"chunk","index":0,"data":"..."}
{"event":"chunk","index":1,"data":"..."}
{"status":"ok","message":"Processed all chunks","result":{...}}
WriteProgress
Typed progress reporting. In TTY mode written to stderr; in agent mode written as NDJSON to stdout. Use Log / log for simple unstructured messages.
for i, item := range items {
w.WriteProgress(murli.ProgressEvent{
Stage: "processing",
Current: i + 1,
Total: len(items),
Percent: float64(i+1) / float64(len(items)) * 100,
EtaMs: estimatedRemainingMs,
Message: item.Name,
})
process(item)
}
w.WriteSuccess("Done", summary)
for (i, item) in items.iter().enumerate() {
writer.write_progress(ProgressEvent {
stage: "processing".into(),
current: i + 1,
total: items.len(),
percent: (i + 1) as f64 / items.len() as f64 * 100.0,
message: item.name.clone(),
..Default::default()
});
process(item);
}
writer.write_success("Done", &json!(summary));
for i, item in enumerate(items):
writer.write_progress(murli.ProgressEvent(
stage="processing",
current=i + 1,
total=len(items),
percent=(i + 1) / len(items) * 100,
message=item.name,
))
process(item)
writer.write_success("Done", summary)
WritePlan — dry-run
Use when the handler is running in dry-run mode. Writes a plan envelope and returns cleanly without performing mutations.
if w.IsDryRun() {
w.WritePlan("Would delete 3 files", map[string]any{
"would_delete": filesToDelete,
"count": len(filesToDelete),
})
return nil
}
for _, f := range filesToDelete { os.Remove(f) }
w.WriteSuccess("Deleted 3 files", nil)
if writer.is_dry_run() {
writer.write_plan("Would delete 3 files", &json!({
"would_delete": files_to_delete,
"count": files_to_delete.len(),
}));
return;
}
for f in &files_to_delete { std::fs::remove_file(f).ok(); }
writer.write_success("Deleted 3 files", &json!(null));
if writer.is_dry_run():
writer.write_plan("Would delete 3 files", {
"would_delete": files_to_delete,
"count": len(files_to_delete),
})
return
for f in files_to_delete:
os.remove(f)
writer.write_success("Deleted 3 files", None)
Log
Writes a diagnostic line to stderr. Consecutive identical lines are deduplicated — safe to call in tight loops.
w.Log("connecting to index...")
w.Progress("[1/3] reading corpus") // overwrites previous progress line at TTY
writer.log("connecting to index...");
// repeated identical calls are deduplicated
writer.log("connecting to index...")
# repeated identical calls are deduplicated
FormatAgentsMD
murli.FormatAgentsMD generates an AGENTS.md stub from the describe output of your CLI. Go only.
describeOut := murliCobra.BuildDescribeTree(rootCmd)
agentsMD := murli.FormatAgentsMD(murli.DescribeOutput{
Name: rootCmd.Use,
Commands: []murli.DescribeCommandSchema{describeOut},
})
fmt.Print(agentsMD)
For Rust and Python, use mytool describe --agents-md > AGENTS.md directly from the CLI.