You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix(workflows): command step crashes with AttributeError on input: null #3492
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.
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", {})
# ...forkey, valueininput_data.items(): # <-- crashes when input is null / list / strresolved_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: drafttype: commandcommand: /speckit.specifyinput: # <-- 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:
In validate(): reject non-mapping input (allow absence) with a clear error, e.g. input must be a mapping, got <type>.
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.
This appears to be already fixed on main — both halves of it. Verified just now against the issue's own reproduction snippet:
input=None -> status=failed err=Command step 't': 'input' must be a mapping, got NoneType.
input=['a'] -> status=failed err=Command step 't': 'input' must be a mapping, got list.
input='oops' -> status=failed err=Command step 't': 'input' must be a mapping, got str.
input=5 -> status=failed err=Command step 't': 'input' must be a mapping, got int.
No AttributeError — each returns a clean FAILED with the contract error, so continue_on_error / per-step handling still works.
execute() guards it too, since WorkflowEngine does not auto-validate before running, so an unvalidated workflow can still reach the step. That's the guard producing the output above.
The sibling gaps you referenced went the same way (#3270's bool max_iterations, etc.). Might be worth closing this one — happy to be corrected if there's a variant still reproducing for you.
Activity
thejesh23 commentedon Jul 13, 2026
Fix up at #3495.
jawwad-ali commentedon Jul 28, 2026
This appears to be already fixed on
main— both halves of it. Verified just now against the issue's own reproduction snippet:No
AttributeError— each returns a cleanFAILEDwith the contract error, socontinue_on_error/ per-step handling still works.Coverage now exists on both paths you identified:
validate()rejects a non-mappinginput(andoptions) — landed in fix(workflows): validate command step input/options are mappings #3262.execute()guards it too, sinceWorkflowEnginedoes not auto-validate before running, so an unvalidated workflow can still reach the step. That's the guard producing the output above.The sibling gaps you referenced went the same way (#3270's bool
max_iterations, etc.). Might be worth closing this one — happy to be corrected if there's a variant still reproducing for you.thejesh23 commentedon Jul 28, 2026
Closing this .