Skip to content

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

@PolyphonyRequiem

Description

@PolyphonyRequiem

Provenance: AI-drafted (GitHub Copilot CLI / Claude Opus 4.6), human-reviewed and approved, AI-submitted.

Problem

Today, sub-workflows (type: workflow) always receive the parent's workflow.input.* values verbatim. The child's workflow.input.X equals the parent's workflow.input.X. There is no way to pass intermediate results (upstream agent outputs) into a sub-workflow.

This makes sub-workflows useful only for phases that need the same static inputs as the parent. You can't call a sub-workflow in a loop with different parameters each iteration — e.g., "plan issue A" then "plan issue B" — because the sub-workflow sees the same workflow.input.* both times.

Current code (src/conductor/engine/workflow.py):

workflow_ctx = context.get("workflow", {})
sub_inputs: dict[str, Any] = (
    dict(workflow_ctx.get("input", {})) if isinstance(workflow_ctx, dict) else {}
)
return await child_engine.run(sub_inputs)

Proposed solution

Add an optional input_mapping field to type: workflow agents. Each key is a sub-workflow input name; each value is a Jinja2 expression evaluated in the parent's context.

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 }}"
      description: "{{ task_manager.output.current_issue_description }}"
    output:
      plan_path: { type: string }
      plan_summary: { type: string }
    routes:
      - to: next_step

Resolution rules:

  1. If input_mapping is present, render each value as a Jinja2 template against the parent's full context, then pass the resulting dict as the sub-workflow's inputs. Parent's workflow.input.* is NOT inherited.
  2. If input_mapping is absent, current behavior is preserved (parent's workflow.input.* is forwarded).

Schema change (AgentDef):

input_mapping: dict[str, str] | None = None
"""Optional mapping of sub-workflow input names to Jinja2 expressions.
Evaluated in the parent context. Only valid for type='workflow'."""

Engine change — roughly:

if agent.input_mapping:
    sub_inputs = {}
    for key, template in agent.input_mapping.items():
        sub_inputs[key] = self._render_template(template, agent_context)
else:
    # existing behavior
    workflow_ctx = context.get("workflow", {})
    sub_inputs = dict(workflow_ctx.get("input", {}))

Validation:

  • input_mapping is only valid when type: workflow
  • Each value must be a valid Jinja2 expression
  • Keys should match the sub-workflow's declared input names (warning, not error)

Why this matters

This is the foundation for all advanced composition patterns. Without it, sub-workflows are limited to "same inputs as parent" which makes them suitable for one-shot phases but not for parameterized or iterative use.

Relationship to other requests

This is Issue 1 of 3 in a series enabling recursive workflow composition:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions