Skip to content

fix(engine): use type-appropriate zero values for optional input defaults#123

Merged
jrob5756 merged 3 commits intomicrosoft:mainfrom
PolyphonyRequiem:fix/optional-input-defaults
May 4, 2026
Merged

fix(engine): use type-appropriate zero values for optional input defaults#123
jrob5756 merged 3 commits intomicrosoft:mainfrom
PolyphonyRequiem:fix/optional-input-defaults

Conversation

@PolyphonyRequiem
Copy link
Copy Markdown
Member

Problem

Optional workflow inputs without an explicit default: value previously defaulted to Python None. This causes two issues:

  1. Templates render "None"{{ workflow.input.optional_msg }} renders as the literal string "None" instead of empty string
  2. | default() doesn't catch None{{ workflow.input.optional_msg | default("fallback") }} still renders "None" because Jinja's default filter only catches undefined, not None (unless boolean=true is passed)

Reproduction

workflow:
  name: test-optional
  entry_point: echo
  input:
    required_id:
      type: number
      required: true
    optional_msg:
      type: string
      required: false
      # no default: specified
agents:
  - name: echo
    type: script
    command: pwsh
    args:
      - "-Command"
      - "Write-Output '{{ workflow.input.optional_msg | default(\"fallback\") }}'; exit 0"
    routes:
      - to: $end

conductor run test.yaml --input required_id=42

Expected: Script outputs fallback (or empty string)
Actual: Script outputs None

Fix

_apply_input_defaults() now uses type-appropriate zero values instead of None for optional inputs with no declared default:

Type Zero value
string ""
number 0
boolean false
array []
object {}

Explicit default: values in the schema are still honored. This is a backward-compatible change — templates that checked if workflow.input.X will still work since zero values are falsy.

Changes

  • src/conductor/engine/workflow.py: Add _TYPE_ZERO_VALUES class constant, update _apply_input_defaults() to use it
  • tests/test_engine/test_workflow.py: Test verifying optional inputs render cleanly (no "None")

Testing

  • 75 workflow engine tests pass (including new test)

…ults

Optional workflow inputs without an explicit `default:` previously
defaulted to Python None, which renders as "None" in templates and
isn't caught by Jinja's `| default()` filter without the boolean flag.

Now uses type-appropriate zero values: "" for string, 0 for number,
false for boolean, [] for array, {} for object. This ensures templates
render cleanly without requiring `| default()` guards or `if X else Y`
workarounds.

Explicit `default:` values in the schema are still honored.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Apr 25, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (main@dbcf20f). Learn more about missing BASE report.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #123   +/-   ##
=======================================
  Coverage        ?   84.94%           
=======================================
  Files           ?       53           
  Lines           ?     7424           
  Branches        ?        0           
=======================================
  Hits            ?     6306           
  Misses          ?     1118           
  Partials        ?        0           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Replace shared [] and {} instances in the class-level _TYPE_ZERO_VALUES
dict with a _zero_value_for_type() method that returns fresh copies for
mutable types (array, object). Prevents potential shared-state bugs if
a caller ever mutates the returned default.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Collaborator

@jrob5756 jrob5756 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as before about portability. Other than that, looks good.

optional numbers as 0 (not 'None'), etc.
"""
config = WorkflowConfig(
workflow=WorkflowDef(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as a few other PRs, replace pwsh with sys.executable.

Every other script-type test in this repo uses command=sys.executable with args=["-c", "..."] (see tests/test_engine/test_script_workflow.py:56,87). PowerShell isn't installed on most local dev environments — this test won't even run on a stock macOS/Linux machine without an extra install. The single-quote stdout assertions are also pwsh-specific.

Suggested rewrite (also requires import sys at the top of the file):

command=sys.executable,
args=[
    "-c",
    (
        "print(f'msg={{ workflow.input.optional_msg }}"
        " count={{ workflow.input.optional_count }}"
        " def={{ workflow.input.with_default }}')"
    ),
],

Even better: convert this to a direct unit test of _apply_input_defaults / _zero_value_for_type parametrized over all five types — it would drop the subprocess dependency entirely and exercise the boolean, array, and object branches that the current end-to-end test never touches.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both calls (pwsh -> direct unit test, all-five-types coverage) folded into one rewrite in ac928e3. Replaced the e2e test with TestApplyInputDefaults — parametrized over string / number / boolean / array / object directly against _apply_input_defaults, no subprocess, no pwsh. Same class also covers explicit-default-honored, provided-value-passthrough, and required-untouched. Thanks for the nudge to drop the subprocess — much cleaner.

Comment thread tests/test_engine/test_workflow.py Outdated

stdout = engine.context.agent_outputs["echo"]["stdout"].strip()
assert "msg=" in stdout
assert "None" not in stdout # Should never render Python's None
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage gap. This test covers string and number (and the explicit-default path), but not boolean, array, or object. The central claim of the fix is "all five InputDef.type values get a sensible zero," so consider adding assertions for the other three — easiest as a parametrized unit test on _apply_input_defaults directly.

While you're there, a small invariant test on _zero_value_for_type would be worthwhile:

assert engine._zero_value_for_type("array") is not engine._zero_value_for_type("array")
assert engine._zero_value_for_type("object") is not engine._zero_value_for_type("object")

This guards against a future "optimization" that caches the mutable instances and reintroduces the classic shared-mutable-default bug.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added test_zero_value_for_mutable_type_returns_fresh_instance parametrized over array and object in ac928e3 — exactly the is not invariant you suggested. Locks in the fix from the second commit on this branch so a future cache "optimization" can't quietly reintroduce the shared-mutable-default bug. The boolean/array/object coverage gap is closed by the parametrized _apply_input_defaults test in the same commit.

…apply_input_defaults

Addresses review feedback on PR microsoft#123:
- Drops subprocess + pwsh dependency (other script tests use sys.executable)
- Covers all 5 InputDef.type values (string, number, boolean, array, object),
  not just string and number
- Adds invariant test on _zero_value_for_type that mutable types (array,
  object) return fresh instances — guards against a future 'optimization'
  caching the instances and reintroducing the shared-mutable-default bug
- Adds explicit-default-honored, provided-value-passthrough, and
  required-input-untouched cases

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Collaborator

@jrob5756 jrob5756 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All review feedback addressed — tests are notably stronger than originally suggested. CI green. LGTM 🚀

@jrob5756 jrob5756 merged commit 7655dc7 into microsoft:main May 4, 2026
7 checks passed
@PolyphonyRequiem PolyphonyRequiem deleted the fix/optional-input-defaults branch May 4, 2026 20:20
jrob5756 added a commit that referenced this pull request May 5, 2026
…, #121-#123, #125, #129, #130, #131, #139, #141-#144, #146)

CHANGELOG: add 6 newer PRs (#119, #121, #122, #123, #125, #113, #130, #131, #141, #146) to [Unreleased] alongside the previously documented batch.

docs/workflow-syntax.md:
  - Add metadata + instructions fields to the workflow configuration block.
  - Add input_mapping and max_depth to Sub-Workflow Steps; correct stale claims that circular references are rejected and that workflow steps cannot be used in for_each groups.
  - Add 'Sub-workflows in for_each groups' subsection with example.
  - Add JSON stdout auto-parsing note + example to Script Steps output section.
  - Add type-appropriate zero values table to Workflow Inputs.
  - Add 'Workflow Metadata Variables' subsection covering workflow.dir, workflow.file, workflow.name.
  - Update on_start hook context list to include the new workflow.dir/file vars.

docs/cli-reference.md:
  - Document --metadata/-m, --workspace-instructions, and --instructions flags on conductor run.
  - Add 'Metadata and Instructions' examples block.
  - Update conductor validate to describe the new template-reference error/warning checks added in #125.

docs/providers/claude.md, docs/providers/comparison.md:
  - Replace stale 'All models support a 200K token context window' / '200K (all models)' claims with notes that the dashboard now sources context_window_max from each provider's SDK at runtime (#144).

README.md:
  - Refresh the Features list to mention sub-workflow composition, dialog mode, workspace instructions, breadcrumb navigation, and the enhanced validate behavior.
  - Add --metadata, --workspace-instructions, --instructions to the conductor run options table.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jrob5756 added a commit that referenced this pull request May 5, 2026
* docs: changelog + doc updates for unreleased PRs (#100, #109-#111, #119, #121-#123, #125, #129, #130, #131, #139, #141-#144, #146)

CHANGELOG: add 6 newer PRs (#119, #121, #122, #123, #125, #113, #130, #131, #141, #146) to [Unreleased] alongside the previously documented batch.

docs/workflow-syntax.md:
  - Add metadata + instructions fields to the workflow configuration block.
  - Add input_mapping and max_depth to Sub-Workflow Steps; correct stale claims that circular references are rejected and that workflow steps cannot be used in for_each groups.
  - Add 'Sub-workflows in for_each groups' subsection with example.
  - Add JSON stdout auto-parsing note + example to Script Steps output section.
  - Add type-appropriate zero values table to Workflow Inputs.
  - Add 'Workflow Metadata Variables' subsection covering workflow.dir, workflow.file, workflow.name.
  - Update on_start hook context list to include the new workflow.dir/file vars.

docs/cli-reference.md:
  - Document --metadata/-m, --workspace-instructions, and --instructions flags on conductor run.
  - Add 'Metadata and Instructions' examples block.
  - Update conductor validate to describe the new template-reference error/warning checks added in #125.

docs/providers/claude.md, docs/providers/comparison.md:
  - Replace stale 'All models support a 200K token context window' / '200K (all models)' claims with notes that the dashboard now sources context_window_max from each provider's SDK at runtime (#144).

README.md:
  - Refresh the Features list to mention sub-workflow composition, dialog mode, workspace instructions, breadcrumb navigation, and the enhanced validate behavior.
  - Add --metadata, --workspace-instructions, --instructions to the conductor run options table.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore: bump version to 0.1.11 and changelog #148

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants