tofupilot validate
Last updated on October 27, 2026
The validate command checks a procedure without running it: the same rules as Studio's Issues panel and as the check tofupilot run makes before it starts. It reports every python: reference that does not resolve, a class or function missing from its file, a plug with: key its class does not take, operator UI and measurement mistakes, and deprecated syntax. It also compiles every Python file of the project, so a syntax error shows before a run imports it: with the procedure's venv, or without one (a CI checkout) with the Python version the run would build the venv with, when uv finds it installed.
No phase or plug is imported or run, no venv is created and the dashboard is not contacted (with the venv, resolving a uses: <package>:<path> imports that package, as a run does), so validate is safe in CI and in an agent's edit loop. Without the venv, a uses: <package>:<path> reference cannot be resolved, and the Python sources are not compiled when that Python version is not installed either: each is reported as a not_checked warning, not an error, so --deny-warnings fails a checkout whose Python was never checked. It exits with code 1 when it finds an error and 0 otherwise (warnings do not fail it, unless --deny-warnings); 2 when PATH is not a procedure YAML file, 4 when it does not exist or holds no procedure. A file that does not load reports every unknown field at once, then what else stopped it; the rest of the file is checked once that is fixed.
tofupilot validateerror[plug_class_not_found]: plugs[0].python: class `PowerSuply` not found in plugs/psu.py; did you mean `plugs/psu.py:PowerSupply`?
--> procedure.yaml:15:5
|
15 | python: plugs/psu.py:PowerSuply
| ^
= help: replace with `plugs/psu.py:PowerSupply`
✗ procedure.yaml: 1 error<PATH>
A procedure .yaml file, or a directory: every procedure.yaml under it is checked (a procedure file with another name is checked by naming it). References resolve from the same directory as tofupilot run uses for that file.
tofupilot validate ./procedures/board/procedure.yaml--deny-warnings
The --deny-warnings flag exits with code 1 on a warning too (a deprecated spelling, a reference not checkable without the venv), for a CI job that keeps the procedure clean.
tofupilot validate --deny-warnings--watch
The --watch flag checks again every time a procedure, sequence, operator panel or Python file under the procedure is saved, and names the file that changed. It follows file-system events on the procedure's folders (venvs and caches left out), or looks at the files every 700 ms when the system refuses a watch. Ctrl-C ends it with code 130, SIGTERM with 143, SIGHUP with 129. With --json, each line after the first names the file in changed.
tofupilot validate --watch--json
With --json, validate prints one JSON object. format_version changes only when a field is removed or changes meaning; new fields can appear without it. Each finding has a stable code (plug_class_not_found, phase_missing_param, phase_plug_out_of_scope (a scope: execution setup or teardown phase taking a slot plug), phase_result_not_received (a parameter naming a phase whose result the phase does not receive: it fails at start on every run), missing_field, unknown_field, duplicate_key, unknown_dependency, python_syntax…) to branch on, the file, line and column of the key it is about, the YAML path, the message as the terminal prints it, and a hint with the replacement when the message suggests one. Findings come in file order. ok is false when the exit code is not 0 (--deny-warnings included). loaded is false when the file did not load. Warnings have codes of their own too: python_import_missing (a top-level from .x import name in a phase or plug file naming what that module of the procedure never defines, or a module its own package does not have: from lib.nothere import x), phase_plug_method_unknown (a phase calling a method its plug's class does not have, psu.read_volts()), python_syntax for a file no phase or plug loads nor imports (a script of the repository), phase_scope_ignored (scope: execution on a main phase, which runs per slot anyway), unit_serial_shared (an auto_identify serial with no {run.slot.key} on a multi-slot procedure: every slot records the same unit), measurement_limits_contradict (limits that leave no value between them, >= 5.1 with <= 4.9), measurement_not_recorded (a declared measurement the phase's function never sets), phase_module_not_in_project (a phase python: with no file in the procedure and no package of that name in its venv, imported at run time; a plug's is an error until its package is installed), deprecated, unknown_marker, marker_as_text, secret_in_env, bare_duration (a duration under 1000 written without a unit, read as milliseconds). A bind: measurements.<key> the phase does not declare is ui_bind_undeclared: an error when the bound key is longer than three letters and one edit from a declared one, a warning otherwise. A problem in a phase a sequence call brings in points at the call's uses: line and names the phase (calibration/check), with no hint: the fix goes in the sequence file. A sequence file given as PATH exits with code 2 and names the procedure to check instead: its {inputs.x} markers only resolve against a call's with:. python_checked says whether the Python sources were compiled (python_skipped says why not), package_dir where python: resolves from, name the procedure's name. For a procedure that loads, the object also lists its phases (enabled is false for enabled: false) with their prompts, and a ui_values skeleton: a --ui-values file with a placeholder for every prompt, ready to fill.
{
"type": "validate",
"format_version": "1",
"ok": false,
"errors": 1,
"warnings": 0,
"procedures": [
{
"path": "/work/bench/procedure.yaml",
"diagnostics": [
{
"severity": "error",
"code": "plug_python_invalid",
"message": "plugs[0].python: Python file not found: plugs/psu/PowerSupply.py; did you mean `plugs.psu:PowerSupply`?",
"file": "/work/bench/procedure.yaml",
"line": 5,
"column": 5,
"yaml_path": "plugs[0].python",
"hint": "plugs.psu:PowerSupply"
}
],
"loaded": true,
"phases": [{ "key": "measure", "stage": "main", "depends_on": [], "enabled": true, "prompts": [] }],
"ui_values": {}
}
]
}With --watch --json, each check prints one such line.
In tofupilot deploy
tofupilot deploy runs the reference check before uploading and refuses a procedure that every run on a station would refuse, with the same lines validate prints.
How is this guide?