From 4fb799865525b0701aabf07d438e4031d5a9bac3 Mon Sep 17 00:00:00 2001 From: Daniel Green Date: Mon, 20 Apr 2026 14:39:30 -0700 Subject: [PATCH 1/4] feat(composition): dynamic sub-workflow inputs via input_mapping (#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> --- src/conductor/config/schema.py | 29 ++++++++++ src/conductor/engine/workflow.py | 23 ++++++-- .../test_config/test_workflow_type_schema.py | 55 +++++++++++++++++++ uv.lock | 2 +- 4 files changed, 103 insertions(+), 6 deletions(-) diff --git a/src/conductor/config/schema.py b/src/conductor/config/schema.py index 527c50b..da05b48 100644 --- a/src/conductor/config/schema.py +++ b/src/conductor/config/schema.py @@ -475,6 +475,24 @@ class AgentDef(BaseModel): workflow: ./research-pipeline.yaml """ + input_mapping: dict[str, str] | None = None + """Optional mapping of sub-workflow input names to Jinja2 expressions. + + Each key is a sub-workflow input parameter name. Each value is a Jinja2 + template expression evaluated against the parent workflow's context. + + When present, the rendered values are passed as the sub-workflow's inputs + instead of forwarding the parent's workflow.input.* values. + + Only valid for type='workflow' agents. + + Example:: + + input_mapping: + work_item_id: "{{ task_manager.output.current_issue_id }}" + title: "{{ task_manager.output.current_issue_title }}" + """ + max_session_seconds: float | None = Field(None, ge=1.0) """Maximum wall-clock duration for this agent's session in seconds. @@ -529,6 +547,8 @@ def validate_agent_type(self) -> AgentDef: raise ValueError("human_gate agents require 'options'") if not self.prompt: raise ValueError("human_gate agents require 'prompt'") + if self.input_mapping: + raise ValueError("human_gate agents cannot have 'input_mapping'") elif self.type == "script": if not self.command: raise ValueError("script agents require 'command'") @@ -555,6 +575,8 @@ def validate_agent_type(self) -> AgentDef: raise ValueError("script agents cannot have 'max_agent_iterations'") if self.retry is not None: raise ValueError("script agents cannot have 'retry'") + if self.input_mapping: + raise ValueError("script agents cannot have 'input_mapping'") elif self.type == "workflow": if not self.workflow: raise ValueError("workflow agents require 'workflow' path") @@ -578,6 +600,13 @@ def validate_agent_type(self) -> AgentDef: raise ValueError("workflow agents cannot have 'max_agent_iterations'") if self.retry is not None: raise ValueError("workflow agents cannot have 'retry'") + else: + # Regular agent or human_gate — input_mapping is not valid + if self.input_mapping: + raise ValueError( + f"'{self.type or 'agent'}' agents cannot have 'input_mapping' " + "(only workflow agents support input_mapping)" + ) return self diff --git a/src/conductor/engine/workflow.py b/src/conductor/engine/workflow.py index 484192b..9dd9bc9 100644 --- a/src/conductor/engine/workflow.py +++ b/src/conductor/engine/workflow.py @@ -545,11 +545,24 @@ async def _execute_subworkflow( ) from exc # Build sub-workflow inputs from the parent context - # Extract workflow.input.* values from the parent context - workflow_ctx = context.get("workflow", {}) - sub_inputs: dict[str, Any] = ( - dict(workflow_ctx.get("input", {})) if isinstance(workflow_ctx, dict) else {} - ) + sub_inputs: dict[str, Any] + if agent.input_mapping: + # Dynamic inputs: render each Jinja2 expression against parent context + renderer = TemplateRenderer() + sub_inputs = {} + for key, template_expr in agent.input_mapping.items(): + rendered = renderer.render(template_expr, context) + # Attempt to parse rendered values as JSON for non-string types + try: + sub_inputs[key] = json.loads(rendered) + except (json.JSONDecodeError, ValueError): + sub_inputs[key] = rendered + else: + # Default: forward parent's workflow.input.* values + workflow_ctx = context.get("workflow", {}) + sub_inputs = ( + dict(workflow_ctx.get("input", {})) if isinstance(workflow_ctx, dict) else {} + ) # Create child engine inheriting provider/registry but with deeper depth child_engine = WorkflowEngine( diff --git a/tests/test_config/test_workflow_type_schema.py b/tests/test_config/test_workflow_type_schema.py index 995c21c..e5036fb 100644 --- a/tests/test_config/test_workflow_type_schema.py +++ b/tests/test_config/test_workflow_type_schema.py @@ -284,3 +284,58 @@ def test_workflow_with_routes_to_agents(self) -> None: # Should not raise warnings = validate_workflow_config(config) assert isinstance(warnings, list) + + +class TestInputMapping: + """Tests for input_mapping on workflow agents.""" + + def test_valid_input_mapping(self) -> None: + """Test that input_mapping is accepted on workflow agents.""" + agent = AgentDef( + name="sub_wf", + type="workflow", + workflow="./sub.yaml", + input_mapping={ + "work_item_id": "{{ intake.output.epic_id }}", + "title": "{{ intake.output.epic_title }}", + }, + ) + assert agent.input_mapping is not None + assert len(agent.input_mapping) == 2 + + def test_workflow_without_input_mapping(self) -> None: + """Test that workflow agents work without input_mapping (backward compat).""" + agent = AgentDef(name="sub_wf", type="workflow", workflow="./sub.yaml") + assert agent.input_mapping is None + + def test_input_mapping_on_regular_agent_raises(self) -> None: + """Test that input_mapping on a regular agent raises ValidationError.""" + with pytest.raises(ValidationError, match="input_mapping"): + AgentDef( + name="regular", + prompt="do something", + input_mapping={"key": "{{ value }}"}, + ) + + def test_input_mapping_on_human_gate_raises(self) -> None: + """Test that input_mapping on a human_gate raises ValidationError.""" + with pytest.raises(ValidationError, match="input_mapping"): + AgentDef( + name="gate", + type="human_gate", + prompt="Choose", + options=[ + GateOption(label="Yes", value="yes", route="next"), + ], + input_mapping={"key": "{{ value }}"}, + ) + + def test_input_mapping_on_script_raises(self) -> None: + """Test that input_mapping on a script agent raises ValidationError.""" + with pytest.raises(ValidationError, match="input_mapping"): + AgentDef( + name="script", + type="script", + command="echo hi", + input_mapping={"key": "{{ value }}"}, + ) diff --git a/uv.lock b/uv.lock index ac918e3..44d02b6 100644 --- a/uv.lock +++ b/uv.lock @@ -150,7 +150,7 @@ wheels = [ [[package]] name = "conductor-cli" -version = "0.1.8" +version = "0.1.9" source = { editable = "." } dependencies = [ { name = "anthropic" }, From 7768dc49b616aecd9c425ab7e384696d5bba42db Mon Sep 17 00:00:00 2001 From: Daniel Green Date: Wed, 22 Apr 2026 12:58:03 -0700 Subject: [PATCH 2/4] fix(composition): forward parent agent outputs to sub-workflow context 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> --- src/conductor/engine/workflow.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/conductor/engine/workflow.py b/src/conductor/engine/workflow.py index 9dd9bc9..987cc1b 100644 --- a/src/conductor/engine/workflow.py +++ b/src/conductor/engine/workflow.py @@ -578,6 +578,14 @@ async def _execute_subworkflow( _subworkflow_depth=self._subworkflow_depth + 1, ) + # Inject parent agent outputs into the child workflow's context. + # This allows sub-workflow agents that declare parent agents in their + # input: list (e.g., task_manager.output?) to access parent state + # 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) + return await child_engine.run(sub_inputs) def _get_context_window_for_agent(self, agent: AgentDef) -> int | None: From 16bec73420a9fccc90d5300fc042629c8dff262f Mon Sep 17 00:00:00 2001 From: Daniel Green Date: Mon, 27 Apr 2026 17:29:40 -0700 Subject: [PATCH 3/4] fix(composition): address PR review feedback on input_mapping - 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> --- src/conductor/config/schema.py | 6 +- src/conductor/engine/workflow.py | 24 +- tests/test_engine/test_subworkflow.py | 415 ++++++++++++++++++++++++++ 3 files changed, 428 insertions(+), 17 deletions(-) diff --git a/src/conductor/config/schema.py b/src/conductor/config/schema.py index da05b48..7f73d84 100644 --- a/src/conductor/config/schema.py +++ b/src/conductor/config/schema.py @@ -547,7 +547,7 @@ def validate_agent_type(self) -> AgentDef: raise ValueError("human_gate agents require 'options'") if not self.prompt: raise ValueError("human_gate agents require 'prompt'") - if self.input_mapping: + if self.input_mapping is not None: raise ValueError("human_gate agents cannot have 'input_mapping'") elif self.type == "script": if not self.command: @@ -575,7 +575,7 @@ def validate_agent_type(self) -> AgentDef: raise ValueError("script agents cannot have 'max_agent_iterations'") if self.retry is not None: raise ValueError("script agents cannot have 'retry'") - if self.input_mapping: + if self.input_mapping is not None: raise ValueError("script agents cannot have 'input_mapping'") elif self.type == "workflow": if not self.workflow: @@ -602,7 +602,7 @@ def validate_agent_type(self) -> AgentDef: raise ValueError("workflow agents cannot have 'retry'") else: # Regular agent or human_gate — input_mapping is not valid - if self.input_mapping: + if self.input_mapping is not None: raise ValueError( f"'{self.type or 'agent'}' agents cannot have 'input_mapping' " "(only workflow agents support input_mapping)" diff --git a/src/conductor/engine/workflow.py b/src/conductor/engine/workflow.py index 987cc1b..02e9c33 100644 --- a/src/conductor/engine/workflow.py +++ b/src/conductor/engine/workflow.py @@ -546,17 +546,21 @@ async def _execute_subworkflow( # Build sub-workflow inputs from the parent context sub_inputs: dict[str, Any] - if agent.input_mapping: + if agent.input_mapping is not None: # Dynamic inputs: render each Jinja2 expression against parent context renderer = TemplateRenderer() sub_inputs = {} for key, template_expr in agent.input_mapping.items(): - rendered = renderer.render(template_expr, context) - # Attempt to parse rendered values as JSON for non-string types try: - sub_inputs[key] = json.loads(rendered) - except (json.JSONDecodeError, ValueError): - sub_inputs[key] = rendered + 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 + sub_inputs[key] = rendered else: # Default: forward parent's workflow.input.* values workflow_ctx = context.get("workflow", {}) @@ -578,14 +582,6 @@ async def _execute_subworkflow( _subworkflow_depth=self._subworkflow_depth + 1, ) - # Inject parent agent outputs into the child workflow's context. - # This allows sub-workflow agents that declare parent agents in their - # input: list (e.g., task_manager.output?) to access parent state - # 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) - return await child_engine.run(sub_inputs) def _get_context_window_for_agent(self, agent: AgentDef) -> int | None: diff --git a/tests/test_engine/test_subworkflow.py b/tests/test_engine/test_subworkflow.py index 6ae8c13..f5bf59a 100644 --- a/tests/test_engine/test_subworkflow.py +++ b/tests/test_engine/test_subworkflow.py @@ -525,3 +525,418 @@ def mock_handler(agent, prompt, context): await engine.run({}) assert engine.limits.current_iteration == 1 + + +class TestSubWorkflowInputMapping: + """Tests for input_mapping on sub-workflow agents.""" + + @pytest.mark.asyncio + async def test_input_mapping_renders_expressions(self, tmp_workflow_dir: Path) -> None: + """Test that input_mapping Jinja2 expressions are rendered and passed as strings.""" + _write_yaml( + tmp_workflow_dir / "sub.yaml", + """\ + workflow: + name: sub-wf + entry_point: inner + runtime: + provider: copilot + input: + item_id: + type: string + required: true + title: + type: string + required: true + limits: + max_iterations: 5 + agents: + - name: inner + prompt: "Work on {{ workflow.input.item_id }}: {{ workflow.input.title }}" + routes: + - to: "$end" + output: + result: "{{ inner.output.result }}" + """, + ) + + parent_path = tmp_workflow_dir / "parent.yaml" + parent_path.write_text("dummy", encoding="utf-8") + + config = WorkflowConfig( + workflow=WorkflowDef( + name="parent", + entry_point="setup", + runtime=RuntimeConfig(provider="copilot"), + context=ContextConfig(mode="accumulate"), + limits=LimitsConfig(max_iterations=10), + ), + agents=[ + AgentDef( + name="setup", + prompt="Setup", + routes=[RouteDef(to="sub_wf")], + ), + AgentDef( + name="sub_wf", + type="workflow", + workflow="sub.yaml", + input_mapping={ + "item_id": "{{ setup.output.id }}", + "title": "{{ setup.output.name }}", + }, + routes=[RouteDef(to="$end")], + ), + ], + output={"result": "{{ sub_wf.output.result }}"}, + ) + + received_prompts: list[str] = [] + + def mock_handler(agent, prompt, context): + received_prompts.append(prompt) + if agent.name == "setup": + return {"id": "42", "name": "Fix the bug"} + return {"result": "done"} + + provider = CopilotProvider(mock_handler=mock_handler) + engine = WorkflowEngine(config, provider, workflow_path=parent_path) + result = await engine.run({}) + + assert result["result"] == "done" + # The inner agent should have received the mapped values + assert any("42" in p and "Fix the bug" in p for p in received_prompts) + + @pytest.mark.asyncio + async def test_input_mapping_values_are_strings(self, tmp_workflow_dir: Path) -> None: + """Test that input_mapping passes values as strings (no json.loads coercion). + + The rendered template values are always strings when entering the child + workflow. Output template rendering may coerce them further, so we verify + via the prompt the child agent actually receives. + """ + _write_yaml( + tmp_workflow_dir / "sub.yaml", + """\ + workflow: + name: sub-wf + entry_point: inner + runtime: + provider: copilot + input: + count: + type: string + required: true + flag: + type: string + required: true + limits: + max_iterations: 5 + agents: + - name: inner + prompt: "Count={{ workflow.input.count }} Flag={{ workflow.input.flag }}" + routes: + - to: "$end" + output: + result: "{{ inner.output.result }}" + """, + ) + + parent_path = tmp_workflow_dir / "parent.yaml" + parent_path.write_text("dummy", encoding="utf-8") + + config = WorkflowConfig( + workflow=WorkflowDef( + name="parent", + entry_point="setup", + runtime=RuntimeConfig(provider="copilot"), + context=ContextConfig(mode="accumulate"), + limits=LimitsConfig(max_iterations=10), + ), + agents=[ + AgentDef( + name="setup", + prompt="Setup", + routes=[RouteDef(to="sub_wf")], + ), + AgentDef( + name="sub_wf", + type="workflow", + workflow="sub.yaml", + input_mapping={ + "count": "{{ setup.output.num }}", + "flag": "{{ setup.output.active }}", + }, + routes=[RouteDef(to="$end")], + ), + ], + output={"result": "{{ sub_wf.output.result }}"}, + ) + + received_prompts: list[str] = [] + + def mock_handler(agent, prompt, context): + received_prompts.append(prompt) + if agent.name == "setup": + return {"num": "42", "active": "true"} + return {"result": "ok"} + + provider = CopilotProvider(mock_handler=mock_handler) + engine = WorkflowEngine(config, provider, workflow_path=parent_path) + await engine.run({}) + + # The child's inner agent should see the string values rendered into the prompt + inner_prompt = [p for p in received_prompts if "Count=" in p][0] + assert "Count=42" in inner_prompt + assert "Flag=true" in inner_prompt + + @pytest.mark.asyncio + async def test_no_input_mapping_forwards_parent_inputs(self, tmp_workflow_dir: Path) -> None: + """Test backward compat: no input_mapping forwards parent workflow.input.*.""" + _write_yaml( + tmp_workflow_dir / "sub.yaml", + """\ + workflow: + name: sub-wf + entry_point: inner + runtime: + provider: copilot + input: + topic: + type: string + required: false + default: "default" + limits: + max_iterations: 5 + agents: + - name: inner + prompt: "Work on {{ workflow.input.topic }}" + routes: + - to: "$end" + output: + topic: "{{ workflow.input.topic }}" + """, + ) + + parent_path = tmp_workflow_dir / "parent.yaml" + parent_path.write_text("dummy", encoding="utf-8") + + config = WorkflowConfig( + workflow=WorkflowDef( + name="parent", + entry_point="sub_wf", + runtime=RuntimeConfig(provider="copilot"), + context=ContextConfig(mode="accumulate"), + limits=LimitsConfig(max_iterations=10), + ), + agents=[ + AgentDef( + name="sub_wf", + type="workflow", + workflow="sub.yaml", + # No input_mapping — should forward parent's workflow.input.* + routes=[RouteDef(to="$end")], + ), + ], + output={"topic": "{{ sub_wf.output.topic }}"}, + ) + + def mock_handler(agent, prompt, context): + return {"result": "ok"} + + provider = CopilotProvider(mock_handler=mock_handler) + engine = WorkflowEngine(config, provider, workflow_path=parent_path) + result = await engine.run({"topic": "Python"}) + + # Parent's workflow.input.topic should be forwarded to child + assert result["topic"] == "Python" + + @pytest.mark.asyncio + async def test_empty_input_mapping_passes_nothing(self, tmp_workflow_dir: Path) -> None: + """Test that input_mapping: {} means 'pass no inputs' (not default forwarding).""" + _write_yaml( + tmp_workflow_dir / "sub.yaml", + """\ + workflow: + name: sub-wf + entry_point: inner + runtime: + provider: copilot + input: + topic: + type: string + required: false + default: "fallback" + limits: + max_iterations: 5 + agents: + - name: inner + prompt: "Work on {{ workflow.input.topic }}" + routes: + - to: "$end" + output: + topic: "{{ workflow.input.topic }}" + """, + ) + + parent_path = tmp_workflow_dir / "parent.yaml" + parent_path.write_text("dummy", encoding="utf-8") + + config = WorkflowConfig( + workflow=WorkflowDef( + name="parent", + entry_point="sub_wf", + runtime=RuntimeConfig(provider="copilot"), + context=ContextConfig(mode="accumulate"), + limits=LimitsConfig(max_iterations=10), + ), + agents=[ + AgentDef( + name="sub_wf", + type="workflow", + workflow="sub.yaml", + input_mapping={}, # Explicitly empty — pass nothing + routes=[RouteDef(to="$end")], + ), + ], + output={"topic": "{{ sub_wf.output.topic }}"}, + ) + + def mock_handler(agent, prompt, context): + return {"result": "ok"} + + provider = CopilotProvider(mock_handler=mock_handler) + engine = WorkflowEngine(config, provider, workflow_path=parent_path) + result = await engine.run({"topic": "Python"}) + + # Empty input_mapping = no inputs passed, child should use its default + assert result["topic"] == "fallback" + + @pytest.mark.asyncio + async def test_input_mapping_error_includes_key_name(self, tmp_workflow_dir: Path) -> None: + """Test that template errors include the failing key name.""" + _write_yaml( + tmp_workflow_dir / "sub.yaml", + """\ + workflow: + name: sub-wf + entry_point: inner + runtime: + provider: copilot + input: + value: + type: string + required: true + limits: + max_iterations: 5 + agents: + - name: inner + prompt: "Use {{ workflow.input.value }}" + routes: + - to: "$end" + output: + result: "done" + """, + ) + + parent_path = tmp_workflow_dir / "parent.yaml" + parent_path.write_text("dummy", encoding="utf-8") + + config = WorkflowConfig( + workflow=WorkflowDef( + name="parent", + entry_point="sub_wf", + runtime=RuntimeConfig(provider="copilot"), + context=ContextConfig(mode="accumulate"), + limits=LimitsConfig(max_iterations=10), + ), + agents=[ + AgentDef( + name="sub_wf", + type="workflow", + workflow="sub.yaml", + input_mapping={ + "value": "{{ nonexistent_agent.output.missing }}", + }, + routes=[RouteDef(to="$end")], + ), + ], + ) + + mock_provider = MagicMock() + engine = WorkflowEngine(config, mock_provider, workflow_path=parent_path) + + with pytest.raises(ExecutionError, match="input_mapping key 'value'"): + await engine.run({}) + + @pytest.mark.asyncio + async def test_no_parent_context_leaks_to_child(self, tmp_workflow_dir: Path) -> None: + """Test that parent agent outputs are NOT injected into child context.""" + _write_yaml( + tmp_workflow_dir / "sub.yaml", + """\ + workflow: + name: sub-wf + entry_point: inner + runtime: + provider: copilot + input: + data: + type: string + required: true + limits: + max_iterations: 5 + agents: + - name: inner + prompt: "Use {{ workflow.input.data }}" + routes: + - to: "$end" + output: + result: "{{ inner.output.result }}" + """, + ) + + parent_path = tmp_workflow_dir / "parent.yaml" + parent_path.write_text("dummy", encoding="utf-8") + + config = WorkflowConfig( + workflow=WorkflowDef( + name="parent", + entry_point="setup", + runtime=RuntimeConfig(provider="copilot"), + context=ContextConfig(mode="accumulate"), + limits=LimitsConfig(max_iterations=10), + ), + agents=[ + AgentDef( + name="setup", + prompt="Setup", + routes=[RouteDef(to="sub_wf")], + ), + AgentDef( + name="sub_wf", + type="workflow", + workflow="sub.yaml", + input_mapping={"data": "{{ setup.output.value }}"}, + routes=[RouteDef(to="$end")], + ), + ], + output={"result": "{{ sub_wf.output.result }}"}, + ) + + child_contexts: list[dict] = [] + + def mock_handler(agent, prompt, context): + if agent.name == "setup": + return {"value": "hello"} + # Capture what the child's inner agent can see + child_contexts.append(dict(context)) + return {"result": "done"} + + provider = CopilotProvider(mock_handler=mock_handler) + engine = WorkflowEngine(config, provider, workflow_path=parent_path) + await engine.run({}) + + # Parent's "setup" agent should NOT appear in child's context + assert len(child_contexts) == 1 + assert "setup" not in child_contexts[0] From 7d01b4287ca867651a9d10d62c24947e82e69fcc Mon Sep 17 00:00:00 2001 From: Daniel Green Date: Mon, 27 Apr 2026 17:46:55 -0700 Subject: [PATCH 4/4] style: fix ruff format on workflow.py Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/conductor/engine/workflow.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/conductor/engine/workflow.py b/src/conductor/engine/workflow.py index 02e9c33..be9f70b 100644 --- a/src/conductor/engine/workflow.py +++ b/src/conductor/engine/workflow.py @@ -555,8 +555,7 @@ async def _execute_subworkflow( 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}", + f"Failed to render input_mapping key '{key}' for agent '{agent.name}': {e}", suggestion=f"Check that the expression '{template_expr}' " "references valid context variables.", ) from e