Connecting one Copilot Studio agent to another from the CLI
A connected agent is an independent, standalone agent that another agent calls as a tool.
A child agent is owned by, and lives inside, its parent. This skill covers connected agents,
built with pac copilot — see copilot-studio-agent for creating the agents themselves and
copilot-studio-mcp-tool for attaching MCP servers.
Verified against pac CLI 2.9.3.
Golden rules
- Build the child first, and publish it. The parent's tool YAML binds by
botSchemaName, so the child's schema name must already exist when you push the parent. - Do not set an output mode you cannot satisfy. A connected-agent tool with
response.mode: Generatedand nooutputs:fails every turn withAgentpluginActionNoOutputSetInEmitMode. Either declare outputs or omit theresponse:block entirely. Omitting it is the right default — the parent orchestrator then writes the final answer, exactly like a working MCP tool. - Name tool files
action.<Name>.mcs.ymlortopic.<Name>.mcs.yml. Any other leading segment is treated as a namespace and gets prepended to every component in the bot. - The child needs
configuration.isAgentConnectable: true(thepac copilot initdefault) and generative orchestration on both sides.
The tool YAML (parent side)
agents/action.<Name>.mcs.yml in the parent workspace:
mcs.metadata:
componentName: LearnDocsAgent
description: Connected agent - learn-docs-agent
kind: TaskDialog
modelDisplayName: learn-docs-agent
modelDescription: <rich description — the orchestrator uses THIS to decide when to delegate>
action:
kind: InvokeConnectedAgentTaskAction
botSchemaName: cr123_learndocsagent
historyType:
kind: ConversationHistory
kind: TaskDialog— same wrapper as MCP and connector tools.action.kind: InvokeConnectedAgentTaskAction— connected agents only. (MCP servers useInvokeExternalAgentTaskAction, connectorsInvokeConnectorTaskAction, flowsInvokeFlowTaskAction, AI Builder promptsInvokeAIBuilderModelTaskAction.)botSchemaNameis the child agent's schema name, e.g. frompac copilot listor the child'ssettings.mcs.yml.historyType.kind: ConversationHistorypasses the conversation so far to the child.- No connection, no connector, no
connectionreference— unlike every other tool kind. That is the whole appeal: agent-to-agent needs no plumbing.
Then:
pac copilot push
pac copilot publish --bot <parentAgentId>
Referencing the connected agent from the parent's instructions
Copilot Studio addresses tool components from instructions with a component path:
instructions: |-
You are the orchestrator agent in a multi-agent workflow.
Make use of the {System.Bot.Components.Agents.'cr123_orchestratoragent.InvokeConnectedAgentTaskAction.action.LearnDocsAgent'.DisplayName}
whenever a user query deals with Microsoft product documentation.
The quoted string is the component's botcomponent.schemaname, not the file name. Push the
tool first, read the real schema name back from Dataverse, then write the instructions:
Invoke-RestMethod -Headers $h -Uri `
"$org/api/data/v9.2/botcomponents?`$select=schemaname&`$filter=_parentbotid_value eq <parentAgentId>"
Plain English ("use the Learn Docs agent") also works and is less brittle; the component path just gives the orchestrator an unambiguous handle.
Inputs and outputs across the agent boundary
Only needed when the parent must pass a specific value in, or read a specific value out. For plain "delegate the question, return the answer", skip this entirely.
Parent side — inputType / outputType go inside action: (for child agents they sit
at the root instead):
kind: TaskDialog
inputs:
- kind: AutomaticTaskInput
propertyName: userEmail
description: The email address of the user to look up
modelDisplayName: HR Specialist
modelDescription: Helps with HR information, including user role
outputs:
- propertyName: userRole
action:
kind: InvokeConnectedAgentTaskAction
inputType:
properties:
userEmail:
displayName: userEmail
isRequired: true
type: String
outputType:
properties:
userRole:
displayName: userRole
description: The role of the user in the organization
type: String
botSchemaName: cr123_hrSpecialist
historyType:
kind: ConversationHistory
Child side — values cross the boundary through global variables, not the topic
outputType. In the child, make the variable global and tick External source can set the
value (inputs) / External source can receive the value (outputs). A child typically has an
OnRecognizedIntent topic for standalone use and an OnRedirect topic that fires when another
agent calls it; both should set Global.<name>.
Component schema-name traps in pac 2.9.3
The agents/ folder is not a free-form directory. Two failures show up immediately:
Namespace bleed. A file named InvokeConnectedAgentTaskAction.LearnDocsAgent.mcs.yml
causes pac to treat InvokeConnectedAgentTaskAction as a namespace and prepend it to every
component in the bot:
cr123_orchestratoragent.InvokeConnectedAgentTaskAction.action.MSNWeather-Getcurrentweather
Over-length schema name. botcomponent.schemaname is capped at 100 characters. Namespace
bleed plus a long agent schema name blows through it:
[0x80044331:StringLengthTooLong] The length of the 'schemaname' attribute of the
'botcomponent' entity exceeded the maximum allowed length of '100'.
Keep every file in agents/ named action.<ShortName>.mcs.yml or topic.<ShortName>.mcs.yml.
The schema-name segment is cosmetic — a connected agent behaves identically whether its
component is called ...action.LearnDocsAgent or ...InvokeConnectedAgentTaskAction.LearnDocsAgent
(the latter is what the Copilot Studio UI produces).
pac copilot pull rewrites connectionreferences.mcs.yml from what the server knows, and a
push then deletes any Dataverse connectionreference row that is not listed in the file. If the
parent also has connector/MCP tools, re-add their entries after every pull, and re-create the
row if it disappeared, before pushing.
Fixing AgentpluginActionNoOutputSetInEmitMode
<Connected agent name> — Failed
AgentpluginActionNoOutputSetInEmitMode
The action '<name>' with output mode 'Generate a Message' must have at least one output set.
The tool runs, the user sees a plausible answer (the parent orchestrator composes its own reply), but the tool step is marked Failed on every turn.
Cause: response.mode: Generated with no outputs:.
kind: TaskDialog
response:
activity:
mode: Generated # emit mode - REQUIRES outputs
modelDisplayName: Learn Docs Agent
action:
kind: InvokeConnectedAgentTaskAction
botSchemaName: cr123_LearnDocsAgent
historyType:
kind: ConversationHistory
Two fixes:
| Fix | What to do | When |
|---|---|---|
| Drop emit mode (recommended) | Delete the whole response: block; push; publish. In the portal: the tool's Outputs section → Output mode → anything other than Generate a message. |
Almost always. The child already produces a natural-language answer. |
| Declare an output | Add outputs: - propertyName: <name> plus action.outputType, and populate a global variable in the child. |
Only when the parent genuinely needs a structured value back. |
Republish after either change. The portal's connected-agent pane does not fully expose inputs/outputs, so the second fix generally has to be done in YAML.
Verify
# tool component landed and points at the right child
Invoke-RestMethod -Headers $h -Uri `
"$org/api/data/v9.2/botcomponents?`$select=schemaname,data&`$filter=_parentbotid_value eq <parentAgentId>"
# child is connectable and published
$b = Invoke-RestMethod -Headers $h -Uri "$org/api/data/v9.2/bots(<childAgentId>)?`$select=name,publishedon,configuration"
($b.configuration | ConvertFrom-Json).isAgentConnectable # expect True
Structural checks do not prove delegation happens. Use the copilot-studio-agent-test skill
to hold a real conversation, and assert both that the child's own tool citations come back
through the parent and that AgentpluginActionNoOutputSetInEmitMode never appears in the
returned activities.
Troubleshooting
| Symptom | Cause |
|---|---|
AgentpluginActionNoOutputSetInEmitMode while answers still look right |
response.mode: Generated with no outputs: — see above |
StringLengthTooLong on botcomponent.schemaname |
Namespace bleed from a tool file not named action.* / topic.* |
| Every component suddenly renamed with an extra segment | Same cause; delete the bad botcomponents rows and re-push with corrected file names |
A record with the specified key values does not exist in connectionreference entity |
A sibling connector/MCP tool lost its connectionreference row after a pull rewrote connectionreferences.mcs.yml |
Push fails with Improper response, not implemented |
Generic pac wrapper error; read %LOCALAPPDATA%\Microsoft\PowerAppsCLI\Microsoft.PowerApps.CLI.<ver>\tools\logs\pac-log.txt for the real message |
| Parent never delegates | modelDescription too vague, generative orchestration off, or the child not published |
| Child answers but ignores passed values | Child reads Topic. variables instead of Global. ones, or External source can set the value is unticked |
Related
copilot-studio-agent— create, configure, publish, and share agents from the CLI.copilot-studio-mcp-tool— attach an MCP server as a tool.copilot-studio-agent-test— prove the whole chain works at runtime.
References
references/WORKED_EXAMPLE.md— a real two-agent build with every value used.