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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions