The plate command provides structured JSON output for programmatic integration
(AI agents, scripts, CI). This document is the human contract; machine-checkable
schemas live in docs/schemas/.
Pass the --json global flag to any command to receive machine-readable JSON on
standard output (stdout). Log messages and warnings go to standard error
(stderr), so you can safely pipe stdout to jq or an agent context window.
--json may appear before or after the subcommand:
plate status --json
plate --json --version
# or without installing:
python3 scripts/bambu.py status --json{
"status": "ok",
"command": "<command>"
}Some commands use a more specific status string ("sliced", "downloaded",
"saved", "sent", "confirmation_required", …). Schema files document the
allowed values per command.
Shared shape: schemas/error_envelope.json.
{
"status": "error",
"command": "slice",
"error": "Timeout waiting for OrcaSlicer to finish.",
"exit_code": 6,
"failed_step": "slicer"
}Job failures may use the superset schemas/job_error.json
(includes summary fields such as would_slice, remote_name, printed,
next_command, recovery_hint).
Path fields under the current home directory are compacted to ~ in agent JSON.
Stable for a given major version (bambu_cli.constants):
| Code | Constant | Meaning |
|---|---|---|
| 0 | EXIT_SUCCESS |
Success |
| 1 | EXIT_CONFIG_ERROR |
Missing/invalid config or required tool |
| 2 | EXIT_NETWORK_ERROR |
Connectivity / MQTT / FTPS failure |
| 3 | EXIT_FILE_ERROR |
Local file I/O |
| 4 | EXIT_PRINTER_ERROR |
Printer reported error |
| 5 | EXIT_COMMAND_ERROR |
Invalid usage / command refused |
| 6 | EXIT_TIMEOUT |
Operation timed out |
Refusal contract: every command that causes physical printer motion or
destroys printer-side data — print, stop, pause, resume, gcode,
delete — refuses without --confirm, exits 5, and (with --json) emits
"status": "confirmation_required" plus a next_command array. job / send
without --confirm is not a refusal: the download/slice/upload really
happened, so it exits 0 with "status": "uploaded_not_printed".
Domain code raises BambuError / abort(); only cli.main() calls sys.exit.
When a print fails with a printer-reported code, the error envelope carries both
printer_error_code (decimal, as the printer sends it) and printer_error_code_hex
(e.g. "0x0500C010"). Bambu documents HMS/print error codes in hex, so use the hex
form when looking a code up; the human-readable log line shows both.
| Schema file | Covers |
|---|---|
ok_envelope.json |
Generic success envelope |
error_envelope.json |
Generic error envelope |
version.json |
--json --version |
status.json |
status --json snapshot |
status_event.json |
status --monitor --json NDJSON events |
doctor.json |
doctor |
preflight.json |
preflight |
config_cmd.json |
config show / config validate |
download.json |
download success |
slice.json |
slice success |
slice_list_settings.json |
slice --list-settings |
job_ok.json |
job / send success / dry-run |
job_error.json |
job / send failure |
print.json |
print (incl. confirmation_required) |
gcode.json |
gcode |
delete.json |
delete |
light.json |
light |
pause.json |
pause |
resume.json |
resume |
stop.json |
stop (incl. confirmation_required) |
snapshot.json |
snapshot |
upload.json |
upload success / --dry-run |
files.json |
files listing |
setup.json |
setup summary |
go.json |
go error envelope (--json always errors) |
Every --json-emitting subcommand now has a dedicated schema. That is enforced,
not asserted: tests/contracts/test_schema_validation.py derives the subcommand
list from build_parser() and fails if any command lacks a schema, if a schema's
$id does not match its filename, if a schema file is unreferenced, or if this
table omits one. Payloads are validated against the published files, mostly from
real --sim invocations rather than hand-written fixtures.
Contract tests: tests/contracts/test_schema_validation.py and
tests/test_json_contracts.py.
{"status": "ok", "command": "version", "version": "X.Y.Z"}version is the installed platecli release (the same string plate --version
prints); it is not a fixed value and is deliberately shown as a placeholder here.
Schema: version.json.
One-shot query returns printer state, temperatures, and a normalized AMS block:
{
"status": "ok",
"command": "status",
"printer": { "...raw printer map..." },
"gcode_state": "IDLE",
"mc_percent": 0,
"ams": null
}ams is either null (no AMS) or:
{
"active_tray": 1,
"units": [
{
"id": 0,
"humidity": 4,
"temp": 28.5,
"trays": [
{"slot": 0, "type": "PLA", "color": "F2F2F2", "remain": 80, "empty": false, "active": false}
]
}
]
}Top-level keys also mirror common fields from the raw printer map for convenience.
printer is always a complete state snapshot. The printer publishes incremental
deltas on its MQTT report topic and answers pushall with the whole state, so
status merges report messages and keeps waiting (re-requesting on each retry)
until gcode_state, mc_percent, bed_temper, and nozzle_temper are all
present. If the printer is too busy to answer with a full snapshot before the
timeout, status fails with exit code 6 and
"failed_step": "status" rather than emitting a partial printer object:
{
"status": "error",
"command": "status",
"exit_code": 6,
"error": "Printer returned only partial status updates, never a full snapshot (missing gcode_state, mc_percent). It may be busy mid-print; retry the command.",
"failed_step": "status",
"detail": {"missing_keys": ["gcode_state", "mc_percent"], "received_keys": ["nozzle_temper"]}
}Streams one JSON object per line until a terminal state. Schema:
status_event.json.
{"event":"update","command":"status","gcode_state":"RUNNING","mc_percent":42}
{"event":"terminal","command":"status","gcode_state":"FINISH","mc_percent":100}Use --sim to exercise the shape without a printer.
Runs connectivity checks and reports the live TLS certificate fingerprint.
Schema: doctor.json. Serial and other secrets are redacted.
{
"status": "ok",
"command": "doctor",
"ok": true,
"output": "/tmp/printer_capabilities.json",
"printer_ip": "<redacted>",
"certificate_fingerprint": "0123456789abcdef...",
"capabilities": { }
}In interactive TTY (not --json), doctor may offer to write cert_fingerprint
into config when none is pinned. That prompt never runs in JSON or non-TTY mode.
Local config/health checks without requiring a full live print path.
Schema: preflight.json. Supports --strict.
Success after OrcaSlicer produces a valid .gcode.3mf (schema: slice.json):
{
"status": "sliced",
"command": "slice",
"file": "~/models/cube.stl",
"path": "~/models/cube.gcode.3mf",
"filename": "cube.gcode.3mf",
"bytes": 4096,
"step_converted": false
}Errors use error_envelope.json. Missing profiles
errors may include profiles_dir and detected_profiles_dir.
slice --list-settings [--json]: slice_list_settings.json.
Without --json, the listing is written to stdout — one [process]/[filament]
key per line, unwrapped — so it can be piped or redirected. The header and the
closing hint go to stderr like other human chrome, which keeps a redirect to a file
free of anything but data:
plate slice --list-settings | grep layer_height
plate slice --list-settings > settings.txtSchema: download.json.
{
"status": "downloaded",
"command": "download",
"source": "https://example.com/model.stl",
"normalized_source": null,
"download_url": "https://example.com/model.stl",
"path": "/tmp/model.stl",
"filename": "model.stl",
"bytes": 1024
}Archive downloads may also include archive_entry. Errors redact source /
download_url. Private IPs are refused unless --allow-private-ips.
config show and config validate share config_cmd.json
(named config_cmd so the schema is not blocked by a config.json gitignore).
Show (secrets redacted):
{
"status": "ok",
"command": "config",
"action": "show",
"config_path": "~/.config/bambu/config.json",
"config": {
"printer_ip": "192.168.1.10",
"access_code": "<redacted>"
}
}Validate includes checks[], ok, errors, warnings, exit_code, and strict.
send is an alias of job.
Success / dry-run: job_ok.json.
Failure: job_error.json.
Print start requires --confirm; without it the job may upload but will not print.
go is the interactive guided-print wizard and has no machine contract: it
requires a TTY and drives real prompts. With --json it never runs the wizard —
it emits the error envelope in go.json (exit_code 5,
failed_step parse) and exits. Piped/non-TTY stdin is refused the same way.
Agents should use job / send with --confirm instead.
Without --confirm (schema: gcode.json):
{
"status": "confirmation_required",
"command": "gcode",
"gcode": "G28",
"sent": false,
"next_command": ["gcode", "G28", "--confirm", "--json"]
}After send: "status": "sent", "sent": true.
Without --confirm (schema: print.json):
{
"status": "confirmation_required",
"command": "print",
"file": "cube.gcode.3mf",
"printed": false,
"next_command": ["print", "cube.gcode.3mf", "--confirm", "--json"]
}Without --confirm (schema: delete.json):
"status": "confirmation_required", "deleted": false.
Requires --confirm (same intent pattern as delete/print). Schema: stop.json.
Without --confirm: "status": "confirmation_required", "stopped": false.
With --confirm: "status": "stopped", "stopped": true.
light.json:"status": "light_changed","action": "on"|"off","changed": truepause.json: dual success-or-confirmation envelope —"status": "paused"with"paused": trueon success, or"status": "confirmation_required"with"paused": falseplusnext_commandwhen--confirmis absentresume.json: dual success-or-confirmation envelope —"status": "resumed"with"resumed": trueon success, or"status": "confirmation_required"with"resumed": falseplusnext_commandwhen--confirmis absent
pause and resume require --confirm (since 0.3.0). Without it they emit
"status": "confirmation_required" with "paused": false / "resumed": false
plus next_command, and exit 5. light does not require --confirm:
it changes no motion, thermal, or material state. See SECURITY.md.
Schema: snapshot.json — "status": "saved", "output",
"size_bytes", "captured_at" (ISO-8601 UTC), "sha256" (hex digest of JPEG bytes),
plus method (direct) or Docker-related fields when the streamer path is used.
Use --unique to get a timestamped filename so repeated captures never overwrite/confuse.
Agents should compare sha256 / captured_at to verify a new frame was captured before
sending the image to a user.
- Timeouts (also optional config keys):
--network-timeout <seconds>--slicer-timeout <seconds>--command-timeout <seconds>--upload-timeout <seconds>
- Security & confinement:
--allow-private-ips: opt in to private/LAN model fetches (default deny). Not sticky config.--max-download-mb: size cap for URL download and ZIP members (default 2048).
- Simulation:
--sim: no real printer traffic for supported paths.
JSON fields documented here and validated under docs/schemas/ are part of the
agent contract. Fields may be added in minor releases. Fields may be
removed or renamed only after a deprecation window of at least one minor
release (or a major version bump). Exit codes in bambu_cli.constants are
stable for a given major version.
Config keys: prefer additive changes; deprecate with a warning (as with inline
access_code) before removal.
Machine-checkable schemas live in docs/schemas/. Contract tests under
tests/contracts/ load those schemas against live CLI output.
- AGENTS.md — agent architecture and safety
- SECURITY.md — threat model
- quality-roadmap.md — quality scoreboard
- test-backlog.md — remaining schema/coverage gaps