Use this skill for service operations only. DO NOT use this skill for CLI implementation lifecycle work such as creating, testing, updating, troubleshooting, validating, removing, or documenting the CLI tool itself; delegate those tasks to cli-tool-expert. Execute Copilot Studio operations using the `copilot` CLI tool. CLI interface for Microsoft Copilot Studio agents via Dataverse API. Triggers: copilot, copilot cli, copilot studio, copilot agent, manage copilot agents, copilot studio agents, copilot topics, copilot knowledge, copilot tools, copilot connectors, power platform copilot, copilot flows, copilot solutions, copilot prompts, copilot models, copilot environment, copilot connections
Resources
1Install
npx skillscat add adbertram/cli-tools/copilot-cli Install via the SkillsCat registry.
| Command | Description |
|---|---|
copilot agent list --table |
List all agents with formatted output |
copilot agent get <id> |
Get agent details by GUID |
copilot agent create --name "Name" |
Create a new agent |
copilot agent prompt <id> -m "msg" |
Send a message to an agent |
copilot agent publish <id> |
Publish agent changes |
copilot agent knowledge list <id> |
List agent knowledge sources |
copilot agent tool list -a <id> |
List tools attached to an agent |
copilot agent topic list -a <id> |
List agent topics |
copilot solution list --table |
List solutions |
copilot auth status |
Check authentication status |
| </quick_start> |
Unexpected character in expression '
"summary_markdown": "..."The CLI handles this automatically: when content contains {, the emitted YAML adds template: { kind: TemplateEngineOptions, format: Mustache } so the publisher uses Mustache (which uses {{ ... }} for expressions) and a single { is plain text. No escaping is needed in the source content. Keep JSON shape examples written naturally.
If you ever see "Unexpected character in expression" on copilot agent publish for an agent whose response-format file or instructions file contains {, suspect the Mustache opt-in is missing โ check client.py build_gpt_component_yaml.
Do NOT set POWERPLATFORM_CLOUD_URL to force a cloud โ that legacy profile value held a Direct Line island-gateway host and is ignored for SDK cloud selection. Forcing the SDK's Other cloud with a host-only value caused M365 SDK request failed: ... PowerPlatformCloud is Other (error code -65003); this is fixed by the auto-resolver.
Override only for a cloud the host table cannot classify: set POWERPLATFORM_CLOUD to a PowerPlatformCloud enum name understood by the SDK (Prod, Gov, High, DoD, Mooncake). A free-form base address is intentionally NOT accepted because the SDK's Other base-address path cannot build a valid connection URL.
Diagnose by running both paths against the same agent minutes apart โ direct fails with the violation, flow succeeds. This proves the agent, auth, and LLM are all fine and the block is a governance rule. The fix is admin-side only: a tenant admin must adjust the environment group's rules (Power Platform admin center โ Manage โ Environment groups โ group โ Rules); non-admin makers get 403 listing the rules via the governance API. Do not retry other client-side transports โ build/keep a minimal flow-based invoke path (copilot agent-flow create --file <def> with an ExecuteCopilotAsyncV2 action) as the policy-compliant route until the rule changes.
Fix: use delegated (user) auth โ run with a profile that has no service-principal secret so the device-code sign-in flow runs โ or have an admin enable app-only S2S access for the environment. This is an environment/tenant policy, not a CLI defect; the CLI now prints this cause and fix when it detects the condition.
This exists specifically to avoid the ~30-day Entra conditional-access sign-in-frequency expiry that delegated az login sessions hit (AADSTS70043) โ service principals have no interactive user session and are not subject to that policy. To activate it for a profile:
- An Azure AD app registration with a client secret (an existing one can be reused โ it does not need
Access Dynamics 365 as organization usersdelegated permission for this path). - A Dataverse Application User for that app, in the target environment, bound to a security role (Power Platform admin center โ environment โ Settings โ Users + permissions โ Application users โ New app user โ search by Application/Client ID โ assign a role). Without this, every call fails
HTTP 403: The user is not a member of the organization.even though MSAL successfully issues a token โ Entra ID and Dataverse are separate trust boundaries here. - Set the profile's
AZURE_CLIENT_ID(Dataverse-specific field โ do not confuse withENTRA_CLIENT_ID, the M365 SDK's public-client field used byagent prompt; both can hold the same app ID but are read by different code paths) andAZURE_CLIENT_SECRET=secret://<name>via the CLI-tools secret manager.
This does NOT extend to copilot agent prompt (Copilot Studio Direct-to-Engine conversations) โ that surface has its own, usually-disabled app-only S2S gate; see the "405 'App-only S2S access is not enabled'" gotcha above. Service-principal auth here only covers the Dataverse Web API command surface, not conversational/runtime agent invocation.
profile="${profile:?set profile from copilot auth profiles list --table}"
if copilot auth status --profile "$profile" >"$auth_out" 2>"$auth_err"; then
auth_rc=0
else
auth_rc=$?
fi
if [ "$auth_rc" -ne 0 ]; then
if [ "$auth_rc" -eq 2 ] && rg -q -F '"authenticated": false' "$auth_out"; then
printf 'COPILOT_AUTH_UNAUTHENTICATED profile=%s rc=%s\n' \
"$profile" "$auth_rc" >&2
cat "$auth_out" >&2
exit 0
fi
printf 'COPILOT_AUTH_STATUS_FAILED rc=%s\n' "$auth_rc" >&2
cat "$auth_err" >&2
exit "$auth_rc"
fiWhen running multiple copilot producers in one shell batch, capture each
producer status immediately after that command and carry the first non-zero
status to the final exit. Do not let later reporting commands such aswc, printf, head, jq, or ls mask a failed producer command.
- Enable Microsoft Teams (no API): publish the agent first (
copilot agent publish <id>), then in Copilot Studio open Channels โ Teams and Microsoft 365 Copilot โ Add channel. Org-wide availability needs admin approval. Docs: https://learn.microsoft.com/microsoft-copilot-studio/publication-add-bot-to-microsoft-teams - Get the Direct Line / Web channel secret (no API): in Copilot Studio open Settings โ Security โ Web channel security, then copy Secret 1 or Secret 2. Teams-only licenses can't generate secrets (tokens are auto-managed). Docs: https://learn.microsoft.com/microsoft-copilot-studio/configure-web-security
- For web embeds, prefer a token over the secret. Run
copilot agent channel get-token <agent>to mint a short-lived Direct Line token (no secret exposure), or exchange a secret for a token server-side viaPOST https://directline.botframework.com/v3/directline/tokens/generate. Never put the Direct Line secret in browser code.
An environment is entitled to publish if EITHER:
- Prepaid Copilot Studio capacity (Copilot Credits) is allocated to it โ one of the currencies
MCSMessages,MCSSessions, orVAConversationshas a positive allocation (GET https://api.powerplatform.com/licensing/environments/{id}/allocations); OR - It is covered by an Enabled pay-as-you-go billing policy (
GET https://api.powerplatform.com/licensing/billingPolicies+ each policy's/environments).
When not entitled, publish exits non-zero with a CapacityError whose message names the environment and lists three fixes: allocate prepaid capacity in the Power Platform admin center (Licensing > Copilot Studio, https://admin.powerplatform.microsoft.com), link a pay-as-you-go billing policy, or use an environment that already has capacity. This is an environment-capacity policy, not a CLI defect โ do not retry; allocate capacity or switch environments. A 404 on the allocations endpoint means "no allocation" (Developer environments), so the env is treated as not entitled. Any other non-200/404 on allocations, or a non-200 on a policy's /environments lookup, is undeterminable and raises an error (the signal cannot be confirmed) rather than silently allowing publish. The target environment id is resolved from DATAVERSE_ENVIRONMENT_ID/POWERPLATFORM_ENVIRONMENT_ID, or matched from DATAVERSE_URL; if neither resolves, the command fails with a clear message.