Skip to content

fix(workflows): command step crashes with AttributeError on input: null #3492

Description

@thejesh23

Summary

The command step in the workflow engine accepts an input: field that is expected to be a mapping. However, a workflow that sets input: null (or omits the value entirely — YAML shorthand for null) crashes at execution with a cryptic AttributeError: 'NoneType' object has no attribute 'items', and validation does not catch it.

This is the same class of type-validation gap that has been getting cleaned up in recent commits — e.g. #3327 (shell timeout), #3270 (loop cap ignores bool max_iterations), #3448 (membership test on non-iterable). Reporting so the command step gets the same treatment.

Reproduction

Minimal repro against main (0.12.13.dev0):

from specify_cli.workflows.steps.command import CommandStep
from specify_cli.workflows.base import StepContext

step = CommandStep()
ctx = StepContext(project_root="/tmp")
step.execute(
    {"id": "test", "command": "/speckit.specify", "input": None},
    ctx,
)

Output:

AttributeError: 'NoneType' object has no attribute 'items'

The offending line is src/specify_cli/workflows/steps/command/__init__.py:37:

input_data = config.get("input", {})
# ...
for key, value in input_data.items():   # <-- crashes when input is null / list / str
    resolved_input[key] = evaluate_expression(value, context)

CommandStep.validate() (same file, line ~152) only checks that command is present — nothing for input. So WorkflowEngine.validate() returns no errors and the crash escapes at run time instead of surfacing as a clean validation error at load time.

The natural YAML that triggers this looks harmless:

- id: draft
  type: command
  command: /speckit.specify
  input:            # <-- YAML parses this as null

Also crashes on input: "some string" and input: [1, 2] for the same reason (.items() on a non-mapping).

Suggested fix

Match the pattern already applied in the shell / while-loop / do-while validators:

  1. In validate(): reject non-mapping input (allow absence) with a clear error, e.g. input must be a mapping, got <type>.
  2. In execute(): defensively coerce input_data to {} when it is not a mapping, so a caller that bypasses WorkflowEngine.validate() still gets a clean skip rather than a crash.

Happy to open a PR — I have a fix + tests ready.


Disclosure: this issue and the accompanying PR were prepared with Claude (Anthropic) assistance. The reproducer was executed against a fresh clone of main and the fix is covered by new unit tests in tests/test_workflows.py.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions