mlcflow CLI Development Guide

Learn how to extend the mlcflow CLI by adding commands, dispatch branches, meta.yaml validation, and error codes.

Sby Skills Guide Bot
DevelopmentAdvanced
007/22/2026
Claude Code
#mlcflow#cli#development#automation#extensibility

Recommended for

skill.md — Claude working guide for mlcflow

Compact task playbooks. Read AGENTS.md for full technical detail.


Mental model

mlcr <tags>  →  mlc_expand_short("run")  →  main()
  →  get_action("script", parent)  →  ScriptAction.run(run_args)
    →  call_script_module_function("run", run_args)
      →  find_target_folder("script")        # walks registered repos
      →  dynamic_import_module(module.py)    # loads ScriptAutomation
      →  ScriptAutomation(self, path).run(run_args)   # engine in mlperf-automations

This repo = CLI driver only. Script logic lives in mlperf-automations/automation/script/module.py, loaded at runtime.

Result dict contract — every action method must return:

  • {'return': 0} on success
  • {'return': N, 'error': 'message'} on failure (N > 0)

Task playbooks

"I need to add a new mlc command"

  1. Add method to the appropriate Action class (hyphens → underscores in name):

    # script_action.py — for a command that delegates to ScriptAutomation
    def my_action(self, run_args):
        return self.call_script_module_function("my_action", run_args)
    
    # script_action.py — for a command handled entirely in mlcflow
    def my_action(self, run_args):
        self.action_type = "script"
        res = self.search(run_args)
        # ... do work ...
        return {'return': 0}
    
  2. Register the command in build_parser() (main.py):

    # General (script/cache/repo): add to the first for-loop ~line 363
    for action in ['run', 'pull', ..., 'my-action']:
    # Script-only: add to the second for-loop ~line 393
    for action in ['docker', ..., 'my-action']:
    
  3. (Optional) Add a short command in main.py + pyproject.toml:

    def mlcx():
        mlc_expand_short("my-action")
    
    mlcx = "mlc.main:mlcx"
    

"I need to add a new dispatch branch for ScriptAutomation"

In call_script_module_function() (script_action.py), add an elif branch:

elif function_name == "my_action":
    result = automation_instance.my_action(run_args)

"I need to add validation for a new meta.yaml key"

In meta_schema.py:

# Add to TOP_LEVEL_SCHEMA (top-level keys):
"my_new_key": DICT,   # or STR, LIST, BOOL, etc.

# Add to DEP_ENTRY_SCHEMA if it goes inside a dep entry
# Add to VARIATION_ENTRY_SCHEMA if it goes inside a variation

Types are sets like STR = {"str"}, LIST = {"list"}, etc. validate_meta() is called during indexing — errors halt the index build.

"I need to add a new error code or improve error messages"

In error_codes.py:

class ErrorCode(Enum):
    MY_ERROR = (2008, "Description of the error")

To show actionable suggestions to the user, add a branch in get_error_guidance():

elif "my error pattern" in message:
    guidance["error_message"] = "Likely cause"
    guidance["suggestions"] = ["Do X to fix it."]

Error codes 2000–2007 are used; warning codes 1000–1006 are used. Pick the next available number in each range.

"I need to understand why mlcr <tags> fails"

mlcr <tags> --verbose                    # full debug output including ScriptAutomation logs
mlc find script --tags=<tags>            # verify script is indexed
mlc reindex                              # rebuild index if stale
mlc list repo                            # verify mlperf-automations is registered
mlc find cache --tags=<failing-dep>      # check if a dep is stale
mlc rm cache --tags=<failing-dep>        # clear dep cache

Check for ScriptExecutionError in the output — it prints a rerun command, the error code, likely cause, suggestions, and version info file path.

"I need to debug why the index is not picking up a script"

  1. Check meta.yaml has all four required keys: alias, uid, automation_alias, automation_uid
  2. Run mlc reindex to force a full rebuild
  3. Check ~/MLC/repos/index_script.json directly if needed
  4. Run mlcr <tags> --verbose — validation errors are logged during indexing

"I need to understand how args reach the automation engine"

convert_args_to_dictionary() in utils.py parses sys.argv extras:

  • --key=val{"key": "val"}
  • --flag{"flag": True}
  • --adr.compiler.tags=gcc{"adr": {"compiler": {"tags": "gcc"}}}
  • -v{"v": True} (unless it matches log_flag_aliases)

The resulting run_args dict is passed directly to ScriptAutomation.run().

"I need to call mlcflow from Python"

from mlc.action import access

result = access({
    'action': 'run',
    'target': 'script',
    'tags': 'detect,os'
})
if result['return'] > 0:
    print(result['error'])

Or use the instance method on an Action object:

from mlc.action import Action
a = Action()
result = a.access({'action': 'find', 'target': 'script', 'tags': 'detect,os'})

"I need to write a test"

Look at existing tests for patterns:

tests/test_cache_mark_tmp.py
tests/test_action_invalid_meta_entries.py

Run tests with:

pip install -e .
python -m pytest tests/

Quick reference: key classes and where they live

| Class / function | File | What it does | |---|---|---| | main() | main.py | CLI entry point; parses args, dispatches to action | | mlcr(), mlcd(), … | main.py | Short-form entry points registered in pyproject.toml | | get_action(target, parent) | action_factory.py | Maps "script"/"cache"/"repo" → Action class | | Action | action.py | Base: repo loading, index, CRUD (add/rm/cp/mv/search) | | Action.access(i) | action.py | Python API: call any action programmatically | | ScriptAction | script_action.py | Script actions; delegates to ScriptAutomation via call_script_module_function | | ScriptExecutionError | script_action.py | Exception with script context; formatted by _report_error() | | RepoAction | repo_action.py | pull/add/rm/find for repos | | CacheAction | cache_action.py | find/rm/show/prune/list/mark-tmp for caches | | Index | index.py | Incremental tag-based index; file-locked JSON | | validate_meta() | meta_schema.py | Validates script meta.yaml; called during indexing | | get_error_guidance() | error_codes.py | Pattern-matches error text → actionable suggestions | | get_new_uid() | utils.py | Generates a 16-hex UID |


Don'ts

  • Don't raise exceptions for recoverable errors in action methods — return {'return': 1, 'error': '...'}
  • Don't call print() for user-facing messages — use logger.info/warning/error
  • Don't add . in flat arg key names — . triggers nested dict parsing in convert_args_to_dictionary()
  • Don't push directly to main — open a PR; use dev only for urgent merges without approval
  • Don't hard-code ~/MLC/repos — use self.repos_path (which reads MLC_REPOS env var)
  • Don't edit index_script.json or modified_times.json by hand — use mlc reindex
  • Don't call ScriptAutomation directly — always go through call_script_module_function()
Related skills