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
workflow add crashes with a traceback when workflow.yml scalars are not strings #3420
validate_workflow() and the workflow add local-install path assume every scalar in workflow.yml is a string. YAML parses unquoted values like 1.0 and 123 as float/int, so a small authoring mistake crashes the CLI with a raw traceback instead of a validation error.
Reproduction
Tested on current main (0.12.9.dev0).
1. Unquoted version — version: 1.0 instead of version: "1.0.0":
workflow:
id: probename: Probeversion: 1.0
File "src/specify_cli/workflows/engine.py", line 152, in validate_workflow
elif not re.match(r"^\d+\.\d+\.\d+$", definition.version):
TypeError: expected string or bytes-like object, got 'float'
2. Numeric workflow id — id: 123:
File "src/specify_cli/workflows/_commands.py", line 604, in _validate_and_install_local
if not definition.id or not definition.id.strip():
AttributeError: 'int' object has no attribute 'strip'
File "src/specify_cli/workflows/engine.py", line 263, in _validate_steps
if ":" in step_id:
TypeError: argument of type 'int' is not iterable
Related:schema_version: 1.0 (unquoted) doesn't crash but produces a confusing message: Unsupported schema_version 1.0. Expected '1.0'. — the float renders identically to the accepted string.
Expected
Clean validation errors from workflow validate/workflow add, e.g. Workflow version 1.0 must be a string — quote it in YAML (version: "1.0.0").
Root cause
validate_workflow() in src/specify_cli/workflows/engine.py regex-matches and iterates definition.id, definition.version and step id values without checking they are strings first. Same for the definition.id.strip() call in _validate_and_install_local. Since this is the shared validation path, the crash reaches workflow add, workflow validate and catalog installs alike.
Reported by @marcelsafin on 2026-07-08. When a workflow.yml contains unquoted numeric YAML values (e.g. version: 1.0, id: 123, or steps: [{id: 123, ...}]), the CLI crashes with a raw Python traceback instead of emitting a clean validation error. Three distinct crash sites are identified, plus a fourth edge case that produces a misleading (but non-crashing) message.
Symptom
When YAML parses unquoted scalars as float or int, validate_workflow() and the workflow add local-install path call string-only operations (re.match, str.strip, in containment) on those values without first checking their type, producing uncaught TypeError / AttributeError exceptions. The user sees a raw traceback instead of a friendly validation message.
Reproduction
Create a workflow.yml with version: 1.0 (unquoted float):
workflow:
id: probename: Probeversion: 1.0
Run specify workflow add <path> or specify workflow validate <path>.
Actual: TypeError: expected string or bytes-like object, got 'float' at engine.py:152.
Expected: Workflow version must be a string — quote it in YAML (version: "1.0.0").
Variant A — id: 123: AttributeError: 'int' object has no attribute 'strip' at _commands.py:604.
Variant B — step id: 123: TypeError: argument of type 'int' is not iterable at engine.py:263.
Variant C — schema_version: 1.0: no crash but prints Unsupported schema_version 1.0. Expected '1.0'. — the float renders identically to the accepted string, so the message is confusing but technically correct.
Suspected Code Paths
src/specify_cli/workflows/engine.py:141 — _ID_PATTERN.match(definition.id) is called after if not definition.id: but with no isinstance(str, ...) guard; a non-string truthy value (e.g. 123) passes the falsy check and then crashes re.match.
src/specify_cli/workflows/engine.py:152 — re.match(r"^\d+\.\d+\.\d+$", definition.version) crashes when definition.version is a float.
src/specify_cli/workflows/engine.py:263 — if ":" in step_id: crashes when step_id is an int (containment test on non-string).
src/specify_cli/workflows/_commands.py:604 — definition.id.strip() crashes when definition.id is an int.
src/specify_cli/workflows/engine.py:132 — definition.schema_version not in ("1.0", "1") — does not crash but yields the misleading float-vs-string mismatch message.
Root Cause Hypothesis
validate_workflow() and the pre-validation guard in _validate_and_install_local assume all top-level and step scalar fields are already str after YAML parsing. PyYAML's default safe_load follows the YAML 1.1 core schema and silently coerces unquoted 1.0 → float and 123 → int. No type guard exists at any of the four call sites before string operations are performed. Confidence: high — all crash sites are confirmed by the reporter's tracebacks and by reading the current source.
Proposed Remediation
Preferred: Add isinstance checks at the top of validate_workflow() for definition.id, definition.version, and definition.schema_version, and at the top of _validate_steps() for each step_id, before any string operation is attempted. When the check fails, append a human-friendly error message (e.g. "Workflow 'id' must be a string — got int; quote it in YAML.") and continue or return errors early so the rest of validation still runs where safe. The pre-validation guard in _validate_and_install_local (definition.id.strip()) should also be guarded or removed (it is redundant since validate_workflow already checks definition.id).
Alternatives:
Coerce all top-level scalars to str immediately after WorkflowDefinition.from_yaml() (e.g. in the dataclass constructor or from_yaml). Trade-off: silently accepts bad input without telling the author what was wrong; also changes the repr in error messages.
Switch to yaml.load(..., Loader=yaml.BaseLoader) which leaves all scalars as strings. Trade-off: would change how booleans and numbers in inputs.default are handled; wider blast radius.
Files likely to change:
src/specify_cli/workflows/engine.py — add isinstance(str) guards in validate_workflow() (lines ~139–155) and _validate_steps() (line ~263).
src/specify_cli/workflows/_commands.py — remove or guard the redundant definition.id.strip() pre-check at line 604.
tests/workflows/test_engine.py (or equivalent) — add test cases for each non-string scalar variant.
Tests to add or update:
test_validate_workflow_float_version — version: 1.0 (float) → validation error, no exception.
test_validate_workflow_int_id — id: 123 (int) → validation error, no exception.
test_validate_schema_version_float — schema_version: 1.0 → clear "must be a string" error rather than float-renders-as-string confusion.
Risks & Considerations
Low regression risk: the change tightens validation, not behavior. Workflows with valid string scalars are unaffected.
The fix should ensure all four validation paths emit errors rather than raising, so a single YAML file with multiple type mistakes surfaces all of them at once.
No API breakage: validate_workflow already returns list[str]; callers handle a non-empty list as failure.
Open Questions
None — the report is self-contained and the code confirms all four crash sites.
Posted on behalf of @mnriem by GitHub Copilot (model: claude-sonnet-4.6, autonomous)
Activity
github-actions commentedon Jul 10, 2026
Bug assessment — yaml-scalar-type-crash: Valid · severity medium
Bug Assessment:
workflow addcrashes with traceback when YAML scalars are not stringsyaml-scalar-type-crashworkflow addcrashes with a traceback when workflow.yml scalars are not strings #3420Report (summarized)
Reported by @marcelsafin on 2026-07-08. When a
workflow.ymlcontains unquoted numeric YAML values (e.g.version: 1.0,id: 123, orsteps: [{id: 123, ...}]), the CLI crashes with a raw Python traceback instead of emitting a clean validation error. Three distinct crash sites are identified, plus a fourth edge case that produces a misleading (but non-crashing) message.Symptom
When YAML parses unquoted scalars as
floatorint,validate_workflow()and theworkflow addlocal-install path call string-only operations (re.match,str.strip,incontainment) on those values without first checking their type, producing uncaughtTypeError/AttributeErrorexceptions. The user sees a raw traceback instead of a friendly validation message.Reproduction
workflow.ymlwithversion: 1.0(unquoted float):specify workflow add <path>orspecify workflow validate <path>.TypeError: expected string or bytes-like object, got 'float'atengine.py:152.Workflow version must be a string — quote it in YAML (version: "1.0.0").Variant A —
id: 123:AttributeError: 'int' object has no attribute 'strip'at_commands.py:604.Variant B — step
id: 123:TypeError: argument of type 'int' is not iterableatengine.py:263.Variant C —
schema_version: 1.0: no crash but printsUnsupported schema_version 1.0. Expected '1.0'.— the float renders identically to the accepted string, so the message is confusing but technically correct.Suspected Code Paths
src/specify_cli/workflows/engine.py:141—_ID_PATTERN.match(definition.id)is called afterif not definition.id:but with noisinstance(str, ...)guard; a non-string truthy value (e.g.123) passes the falsy check and then crashesre.match.src/specify_cli/workflows/engine.py:152—re.match(r"^\d+\.\d+\.\d+$", definition.version)crashes whendefinition.versionis afloat.src/specify_cli/workflows/engine.py:263—if ":" in step_id:crashes whenstep_idis anint(containment test on non-string).src/specify_cli/workflows/_commands.py:604—definition.id.strip()crashes whendefinition.idis anint.src/specify_cli/workflows/engine.py:132—definition.schema_version not in ("1.0", "1")— does not crash but yields the misleading float-vs-string mismatch message.Root Cause Hypothesis
validate_workflow()and the pre-validation guard in_validate_and_install_localassume all top-level and step scalar fields are alreadystrafter YAML parsing. PyYAML's defaultsafe_loadfollows the YAML 1.1 core schema and silently coerces unquoted1.0→floatand123→int. No type guard exists at any of the four call sites before string operations are performed. Confidence: high — all crash sites are confirmed by the reporter's tracebacks and by reading the current source.Proposed Remediation
Preferred: Add
isinstancechecks at the top ofvalidate_workflow()fordefinition.id,definition.version, anddefinition.schema_version, and at the top of_validate_steps()for eachstep_id, before any string operation is attempted. When the check fails, append a human-friendly error message (e.g."Workflow 'id' must be a string — got int; quote it in YAML.") andcontinueorreturn errorsearly so the rest of validation still runs where safe. The pre-validation guard in_validate_and_install_local(definition.id.strip()) should also be guarded or removed (it is redundant sincevalidate_workflowalready checksdefinition.id).Alternatives:
strimmediately afterWorkflowDefinition.from_yaml()(e.g. in the dataclass constructor orfrom_yaml). Trade-off: silently accepts bad input without telling the author what was wrong; also changes the repr in error messages.yaml.load(..., Loader=yaml.BaseLoader)which leaves all scalars as strings. Trade-off: would change how booleans and numbers ininputs.defaultare handled; wider blast radius.Files likely to change:
src/specify_cli/workflows/engine.py— addisinstance(str)guards invalidate_workflow()(lines ~139–155) and_validate_steps()(line ~263).src/specify_cli/workflows/_commands.py— remove or guard the redundantdefinition.id.strip()pre-check at line 604.tests/workflows/test_engine.py(or equivalent) — add test cases for each non-string scalar variant.Tests to add or update:
test_validate_workflow_float_version—version: 1.0(float) → validation error, no exception.test_validate_workflow_int_id—id: 123(int) → validation error, no exception.test_validate_steps_int_step_id— stepid: 123(int) → validation error, no exception.test_validate_schema_version_float—schema_version: 1.0→ clear "must be a string" error rather than float-renders-as-string confusion.Risks & Considerations
validate_workflowalready returnslist[str]; callers handle a non-empty list as failure.Open Questions
Posted on behalf of
@mnriemby GitHub Copilot (model: claude-sonnet-4.6, autonomous)fix(workflows): report validation errors instead of crashing on non-s…