Skip to content

feat(composition): dynamic sub-workflow inputs via input_mapping (#101)#109

Open
PolyphonyRequiem wants to merge 4 commits intomicrosoft:mainfrom
PolyphonyRequiem:feat/input-mapping
Open

feat(composition): dynamic sub-workflow inputs via input_mapping (#101)#109
PolyphonyRequiem wants to merge 4 commits intomicrosoft:mainfrom
PolyphonyRequiem:feat/input-mapping

Conversation

@PolyphonyRequiem
Copy link
Copy Markdown
Member

Summary

Adds an optional input_mapping field to type: workflow agents, enabling parameterized sub-workflow invocation. Each key maps a sub-workflow input name to a Jinja2 expression evaluated in the parent's context.

Closes #101

Example

agents:
  - name: plan_issue
    type: workflow
    workflow: ./plan-and-review.yaml
    input_mapping:
      work_item_id: "{{ task_manager.output.current_issue_id }}"
      title: "{{ task_manager.output.current_issue_title }}"

Without input_mapping, parent's workflow.input.* is forwarded (existing behavior preserved).

Changes

  • Schema: input_mapping: dict[str, str] | None on AgentDef, validated only for type: workflow
  • Engine: render input_mapping templates against parent context in _execute_subworkflow
  • Tests included

Dependency

Foundation for #102 (for_each with workflows) and #103 (self-referential workflows).

…rosoft#101)

Add input_mapping field to AgentDef for type='workflow' agents. When
present, each value is a Jinja2 expression rendered against the parent
context to build sub-workflow inputs. When absent, existing behavior
(forwarding parent's workflow.input.*) is preserved.

- Schema: Add input_mapping to AgentDef with validation for workflow-only
- Engine: Render input_mapping templates in _execute_subworkflow()
- Tests: Schema validation for all agent types
- Experimental workflows: test-input-mapping parent/child pair

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

codecov-commenter commented Apr 21, 2026

Codecov Report

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

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #109   +/-   ##
=======================================
  Coverage        ?   84.93%           
=======================================
  Files           ?       53           
  Lines           ?     7182           
  Branches        ?        0           
=======================================
  Hits            ?     6100           
  Misses          ?     1082           
  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.

When a parent workflow calls a sub-workflow via type: workflow, the
child engine now inherits the parent's agent outputs in its context.
This allows sub-workflow agents to access parent agent state (e.g.,
task_manager.output, pr_group_manager.output) by declaring them in
their input: list, even when input_mapping doesn't cover all fields.

Previously, sub-workflow agents could only access workflow.input.*
(populated via input_mapping) and their own sibling agents' outputs.
Parent agent outputs were lost at the sub-workflow boundary, causing
nested sub-workflows to see default values (0, '') instead of the
actual parent state.

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.

Looks good. Some small comments.

Comment thread src/conductor/engine/workflow.py Outdated
# even when input_mapping doesn't cover all fields.
for key, value in context.items():
if key not in ("workflow", "context") and isinstance(value, dict):
child_engine.context.agent_outputs[key] = value.get("output", value)
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.

Critical: Unconditional parent context injection breaks sub-workflow isolation

This block runs for every sub-workflow, not just those with input_mapping. This is a backward-incompatible behavioral change — previously child workflows started with a clean agent_outputs.

Bugs this causes:

  1. Namespace collision: If parent and child both have an agent analyzer, the parent's output is pre-injected. Before the child's own analyzer runs, {{ analyzer.output }} resolves to stale parent data instead of raising an error.
  2. Context bypass: Direct assignment to agent_outputs skips context.store(), so execution_history is out of sync.
  3. State leakage: value.get("output", value) — if no "output" key exists, the entire internal dict (including model, tokens, etc.) is injected.

Fix: Either:

  • Guard with if agent.input_mapping: (make it opt-in), or
  • Remove entirely — input_mapping already provides controlled access to parent data. Blanket injection is redundant and unsafe.

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.

I think this current commit should cover it.

I've removed the parent context injection block entirely.
input_mapping already provides explicit, controlled access to parent data via Jinja2 expressions, making
the blanket injection both redundant and harmful for the reasons you outlined

Comment thread src/conductor/engine/workflow.py Outdated
rendered = renderer.render(template_expr, context)
# Attempt to parse rendered values as JSON for non-string types
try:
sub_inputs[key] = json.loads(rendered)
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.

Critical: Implicit json.loads coercion is a foot-gun

This silently coerces types by content sniffing:

  • "42"int · "true"bool · "null"None

Users have no way to pass the literal string "true" as a string input. The "null"None case is especially dangerous — a missing/empty variable becomes None and the sub-workflow may not catch it.

Suggestion: Remove json.loads and always pass rendered values as strings (matching how --input CLI works). If structured data is needed, let users use the {{ expr | json }} filter explicitly.

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.

was unaware of this, thank you!

Comment thread src/conductor/engine/workflow.py Outdated
renderer = TemplateRenderer()
sub_inputs = {}
for key, template_expr in agent.input_mapping.items():
rendered = renderer.render(template_expr, context)
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.

Important: Template errors lack key context

If renderer.render() raises TemplateError (e.g., undefined variable), the error message won't indicate which input_mapping key failed. With many keys, debugging is a guessing game.

Suggested change
rendered = renderer.render(template_expr, context)
for key, template_expr in agent.input_mapping.items():
try:
rendered = renderer.render(template_expr, context)
except Exception as e:
raise ExecutionError(
f"Failed to render input_mapping key '{key}' for agent "
f"'{agent.name}': {e}",
suggestion=f"Check that the expression '{template_expr}' "
"references valid context variables.",
) from e

Comment thread src/conductor/engine/workflow.py Outdated
dict(workflow_ctx.get("input", {})) if isinstance(workflow_ctx, dict) else {}
)
sub_inputs: dict[str, Any]
if agent.input_mapping:
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.

Suggestion: if agent.input_mapping: — empty dict {} is falsy

An explicit input_mapping: {} in YAML means "pass no inputs", but {} is falsy in Python, so this falls through to the default behavior (forwarding all workflow.input.*). Consider using if agent.input_mapping is not None: to distinguish "not set" from "explicitly empty".

with pytest.raises(ValidationError, match="input_mapping"):
AgentDef(
name="script",
type="script",
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.

Important: No runtime tests for the core feature

These 5 schema tests are good, but there are zero tests for the 34 new runtime lines in workflow.py. Missing coverage:

  1. Jinja2 rendering of input_mapping expressions against parent context
  2. JSON type coercion behavior ("42" → int, "true" → bool)
  3. Error when Jinja2 references an undefined variable
  4. Parent context injection and namespace collision with child agents
  5. End-to-end: parent output → input_mapping → child sub-workflow receives correct inputs
  6. Backward compat: no input_mapping → existing behavior preserved

Recommend adding these in tests/test_engine/test_subworkflow.py using the existing mock provider pattern.

Daniel Green and others added 2 commits April 27, 2026 17:29
- Use 'is not None' guard instead of truthiness check so empty
  input_mapping ({}) means 'pass no inputs' rather than falling
  through to default forwarding (schema validators + runtime)
- Remove implicit json.loads coercion on rendered values; always
  pass strings to match explicit-data-flow intent
- Add per-key error wrapping with key name + expression in error
  messages for easier debugging of template failures
- Remove unconditional parent context injection into child
  workflows to preserve sub-workflow isolation and avoid namespace
  collisions, context.store() bypass, and state leakage
- Add 7 runtime tests covering input_mapping rendering, string
  values, backward compat, empty mapping, error messages, and
  parent context isolation

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.

feat(composition): dynamic sub-workflow inputs via input_mapping

3 participants