Build a Plugin
Build a Plugin
Section titled “Build a Plugin”A plugin is a Microsoft 365 app package that composes a declarative agent, one or more skills, and/or agent connectors (remote MCP servers or references to existing OpenAPI custom connectors) into a single, independently shippable unit. This guide gets you from nothing to a validated, provisioned, and shared plugin — the same outcome the Microsoft Learn “Build plugins for Copilot Cowork” walkthrough targets, reached the wiqd way. If you haven’t yet, read What is a plugin? first for the mental model.
Prerequisites
Section titled “Prerequisites”-
Install wiqd (the CLI, optional VS Code extension, and public WIQD plugin for detected native coding-assistant hosts):
Windows (PowerShell):
Terminal window iex "& { $(irm 'https://aka.ms/wiqd/install.ps1') }"macOS/Linux:
Terminal window curl -fsSL https://aka.ms/wiqd/install.sh | bashSee Installation for flags, troubleshooting, and manual steps.
-
Install the WIQD coding-assistant plugin from its marketplace if you want guided authoring and did not install it in Step 1. This guide plugin is distinct from the Microsoft 365 plugin you will create:
Terminal window copilot plugin install wiqd@awesome-copilotOr use the WIQD marketplace:
Terminal window copilot plugin marketplace add microsoft/wiqdcopilot plugin install wiqd@wiqdFor Claude Code, add
microsoft/wiqdand installwiqd@wiqd. Start a new host session. -
Sign in — provisioning and sharing a plugin touch your Microsoft 365 tenant:
Terminal window wiqd auth login --interactiveSee Authentication for how sign-in works across providers.
-
Verify your environment:
Terminal window wiqd doctor
With that done, you have two paths to the same result. Start with Path 1 — it’s the fastest way to build a plugin and it teaches you the lifecycle as you go. Reach for Path 2 when you need scripting, CI, or exact reproducibility.
Path 1 — Build it conversationally
Section titled “Path 1 — Build it conversationally”How it works
Section titled “How it works”Once the wiqd plugin is installed, GitHub Copilot CLI carries a plugin-authoring orchestrator: you
describe what you want in natural language, and it drives the wiqd CLI on your behalf —
including the full wiqd plugin ... surface — while orienting you on the underlying
Build → Improve → Preview → Publish plugin lifecycle. You never have to remember a flag; you just
say what you want next.
Plugin authoring defaults to shared scope in dev, without asking you to
choose or confirm that default. The assistant passes --env dev explicitly
through source validation, provisioning, packaging, publishing, and sharing.
An explicit personal/local or custom environment, or one already selected in
the conversation, is preserved. This is a guided-authoring default, not a change
to bare CLI commands (which still default to local), and it does not
automatically deploy, share, publish, or delete anything.
What to say
Section titled “What to say”Saying any of these in GitHub Copilot CLI routes the conversation into the plugin-authoring workflow:
create a standalone plugin · build a plugin with a skill · build a plugin with a connector · add a skill to my plugin · add a connector to my plugin · add an agent to my plugin · validate my plugin · package my plugin · share my plugin · show my plugin · list my plugins · import an open plugin · import a claude plugin · export my plugin for claude · export my plugin for cursor · delete my plugin
A conversational walkthrough
Section titled “A conversational walkthrough”You say: “Create a standalone plugin called Triage Helper.”
wiqd does: runs wiqd plugin create --name "Triage Helper", cds into the new project, and
reports back — then suggests the logical next moves: add a skill, add an MCP connector, or add a
declarative agent.
You say: “Add a skill for triaging incoming issues.”
wiqd does: runs wiqd plugin add skill --name "Triage Issues", scaffolding
appPackage/skills/triage-issues/SKILL.md and registering it in the manifest. It then suggests
adding another capability, or validating what you have so far.
You say: “Validate my plugin.”
wiqd does: runs wiqd plugin validate --mode source --env dev (the offline source check). On a clean result, it
suggests provisioning the plugin or exporting it for another tool. There is no shipped command
that merges a whole plugin into an existing agent. For an OpenAPI custom connector
or remote MCP connector using ConnectionVault, it omits export because the pinned
exporter cannot preserve the connector reference, and explains that source validation does not establish packaging or
deployment readiness.
You say: “Provision it, then package and share it with my team.”
wiqd does: explains that standard projects use local for personal testing and dev
for shared deployment, selects dev without a default-selection question, and checks
the project’s actual scope configuration. It validates, runs wiqd plugin provision --env dev, then
wiqd plugin package --env dev, then
wiqd plugin share --env dev --scope users --email <you supply>. Each step’s
report tells you what ran and what’s next, so a multi-step request like this one still gives you a
checkpoint after every command.
If you already provisioned in personal local, asking to share does not make
that deployment shareable. Copilot explains the restriction and asks permission
for any additional cloud provisioning not already authorized, then validates and
prepares the shared environment before sharing. A ready shared deployment is
reused without redundant provisioning. Custom environment names and scope settings
are respected; missing or conflicting configuration is reported rather than
silently replaced. Help and read-only requests do not deploy anything, and
tenant-wide sharing requires explicit audience confirmation.
This mirrors the same create → add → validate → provision → package → share lifecycle you’d run
by hand — the orchestrator just chooses and sequences the commands for you, one reported step at a
time.
Path 2 — Use the CLI directly
Section titled “Path 2 — Use the CLI directly”Reach for the CLI directly instead of the conversational path when you’re scripting a CI/CD pipeline, need fully deterministic and reproducible non-interactive automation, or you simply prefer typing commands yourself. Every command below is the same one the conversational path runs under the hood — nothing here is a different surface, just a different way to drive it.
Choose the deployment scope first. The example below uses the standard shared
dev environment because it ends in sharing. Use local for personal testing,
not sharing. These are scaffold defaults: check the effective lifecycle
configuration and AGENT_SCOPE in the selected environment under
environmentFolderPath (default ./env); an environment name alone does not prove
it is shared. --scope users|tenant selects the sharing audience, not deployment
scope. There is no wiqd plugin provision --scope shared option.
Direct wiqd plugin share (and wiqd plugin remove access) checks the selected environment
file for AGENT_SCOPE=shared before sharing. Missing, invalid, duplicate, or
non-shared scope fails with INVALID_AGENT_SCOPE (exit 1); the CLI does not
provision automatically. Correct the shared deployment configuration, validate,
and provision using the same explicit --path and --env, then retry sharing.
Do not relabel a personal deployment or copy its IDs to bypass this check.
# 1. Scaffold the plugin containerwiqd plugin create --name my-plugincd my-plugin
# 2. Compose whatever capabilities you need, in any combinationwiqd plugin add agentwiqd plugin add skill --name "Triage Issues"wiqd plugin add connector --name "Contoso Tools" \ --description "Contoso toolbelt over MCP" --url https://tools.contoso.com/mcp
# Or reuse a connector the Microsoft 365 catalog already publishes — the catalog# supplies its URL and credential reference. Copy the selector from `connector list`.wiqd connector list --search "Azure DevOps"wiqd plugin add connector --selector "Azure DevOps MCP"
# Or reference an existing custom connector by its logical name.# --dev-preview explicitly updates both manifestVersion and $schema.wiqd plugin add connector --kind openapi --name "Contoso Kusto" \ --description "Queries the existing Contoso Kusto connector" \ --auth-reference-id shared_kusto --dev-preview
# 3. Validate offline before you provision or packagewiqd plugin validate --mode source --env dev
# 4. Provision first — it writes the env file package/share both depend onwiqd plugin provision --env dev
# 5. Build the deployable .zipwiqd plugin package --env dev
# 6. Validate the built ZIP through Core/TDP (uploads the ZIP; sign-in may be required)wiqd plugin validate --env dev --mode package
# 7. Share itwiqd plugin share --env dev --scope users --email you@contoso.com# …or share with your whole tenant:wiqd plugin share --env dev --scope tenant
# Inspect at any pointwiqd plugin showwiqd plugin list --root .
# Tear down what provision created (--yes skips the confirmation prompt for non-interactive use)wiqd plugin delete --env dev --yesAfter provisioning, wiqd prints a Cowork install/test link in this form:
https://copilot.cloud.microsoft/plugins?pluginsTab=cowork&coworkPage=detail&coworkPluginId=<M365_TITLE_ID>Open that link to install and test the standalone plugin in Cowork. After
wiqd plugin provision, wiqd also returns a separate Chat install/test link:
https://copilot.cloud.microsoft/plugins?titleId=<M365_TITLE_ID>&pluginsTab=chatAfter wiqd plugin share, wiqd instead returns these two recipient links:
Cowork: https://m365.cloud.microsoft/cowork#/extensions/plugins/<M365_TITLE_ID>Chat: https://m365.cloud.microsoft/chat/?titleId=<M365_TITLE_ID>Both commands return data.deepLink for Cowork and data.chatLink for Chat.
Both links use the
same complete M365_TITLE_ID returned by the API, preserving any T_ or P_
prefix without adding or converting it. Standalone declarative-agent commands
retain their separate Microsoft 365 Copilot chat route.
The assistant uses the successful command’s returned data.deepLink, including
for plugins containing a declarative agent, and the returned data.chatLink
after provisioning or sharing. It does not replace the Cowork link with a Chat
URL or an older environment’s SHARE_LINK. If either link is missing or unexpected,
it reports the problem rather than inventing a URL from the Teams app ID or
provisioning again.
For every flag on every one of these commands, see
wiqd plugin in the CLI reference rather than duplicating the
full flag set here.
--kind openapi does not author an OpenAPI API-plugin action and does not create a custom
connector. It writes a ConnectionVault pointer to an existing custom connector. The
--auth-reference-id value is its logical name (for example shared_kusto), not a URL, secret,
connection instance, or OAuth registration id. This connector shape is currently available only
through explicit devPreview authoring; wiqd does not assume a tentative numbered manifest version
supports it.
Publishing and existing projects
Section titled “Publishing and existing projects”The walkthrough above ends in sharing: granting people access to a shared deployment.
Publishing is a separate step that submits your plugin to your organization’s catalog.
Even share --scope tenant is sharing, not catalog publication.
After provisioning and validating your plugin, publish using the same explicit environment:
wiqd plugin publish --env devThe command runs the selected environment’s YAML publish: lifecycle. Before publishing
an existing project, check that block: updating wiqd does not rewrite your project files.
For plugin tenant publication, use copilotAgent/publish with scope: tenant, mapping
titleId to M365_PUBLISHED_TITLE_ID and appId to M365_PUBLISHED_APP_ID.
These keep publication IDs separate from the provisioning IDs used by the install/test
link above. Tenant publishing does not generate a share link.
If you use --manifest, its path must match every teamsApp/zipAppPackage manifest path
in the selected publish: block. A mismatch or an unresolved environment token in those
paths fails before submission with UNSUPPORTED_PACKAGING_MANIFEST.
Import / export (interop)
Section titled “Import / export (interop)”Already have a plugin authored for another host, or want to hand yours to one? Two commands bracket the lifecycle instead of requiring a hand-rolled conversion script:
# Bring a foreign-format plugin into wiqd instead of starting from `create`wiqd plugin import --path ./my-open-plugin \ --privacy-url https://contoso.com/privacy \ --terms-url https://contoso.com/terms
# Hand a wiqd plugin project to another hostwiqd plugin export --format claude-pluginwiqd plugin import recognizes an Open Plugin, Claude plugin, or Cursor plugin source and produces
a new wiqd plugin project from it. --privacy-url/--terms-url are only required on a plugin’s
first import — if the source was produced by a prior wiqd plugin export, the round-trip
metadata already carries them. wiqd plugin export does the inverse, defaulting to
--format open-plugin (also accepts claude-plugin and cursor-plugin), writing an uncompressed
directory under <path>/export/<format>. The output layout uses the core backend:
writes the selected format layout; FxCore always writes plugin.json at the export root and uses
--format only for the default output directory and reported format.
The imported project is ready for the full lifecycle. The TeamsFx importer creates its deployment
environment (currently dev), and wiqd binds the package manifest to that environment’s
TEAMS_APP_ID. For a default import with no app ID, continue with wiqd plugin validate --mode source --env dev, then run
wiqd plugin provision --env dev before wiqd plugin package --env dev,
wiqd plugin publish --env dev, wiqd plugin share --env dev, or package validation. Packaging through either backend
fails closed while the selected environment file is missing or its TEAMS_APP_ID is empty,
unresolved, or malformed, instead of allowing a random package-only ID.
Mock import follows the same finalized project contract: the package manifest keeps the
${{TEAMS_APP_ID}} binding, the generated dev environment is present, and an explicit
--app-id is written to that environment before provision/package checks run.
If you pass --app-id, or a supported source extension carries a real GUID, import writes that ID
into .env.dev; the project is already identity-bound for packaging. A wiqd export does not invent
or preserve an unresolved ${{TEAMS_APP_ID}} token as a round-trip ID, so only a real GUID from a
supported producer qualifies. Provision/update remains the normal tenant lifecycle step before
sharing a newly imported app.
Validation as a first-class step
Section titled “Validation as a first-class step”Rather than assembling a package and hoping upload-time validation passes, validate continuously as you build:
- Source (
--mode source) — offline saved declarative-agent diagnostics. The project must contain validm365agents.ymlandappPackage/manifest.json. It does not fully validate those schemas, skill/connector content or referenced API/OpenAPI content. A skill-only/connector-only plugin reports no DA source coverage; a declared but missing DA fails. Use this explicitly before provisioning or packaging. - Package (default,
--mode package) — checks the final ZIP through Teams Developer Portal. It uploads the ZIP and may require sign-in; it does not build the ZIP or sign in automatically. - Marketplace (
--mode marketplace) — package checks plus StoreOps policy/evidence checks on the same captured ZIP. An individual policy with missing evidence remains unverified; completed validation without confirmed must-fix findings can still bePASSED. Incomplete provider execution isERRORunless confirmed blockers make the overall resultFAILED. A report is not official Store approval.
Use wiqd plugin validate and read
validation.result for the overall verdict. Package/Marketplace write
immutable JSON/HTML reports and accept a ZIP without a source project. Supply --package-file <zip>
or a structurally recognized project containing exactly one appPackage/build/*.zip.
Old static/deep/manifest modes
are rejected with migration guidance; the default is no longer offline.
Source validation does not inspect root agentConnectors[], including openApiConnector.
For a preview custom connector, package before package validation. Local devPreview
packaging supports OpenAPI and remote MCP
connectors using ConnectionVault, preserving their fields and leaving the source manifest
unchanged. Local packaging success is not a server verdict. Check the built ZIP with
wiqd plugin validate --mode package --package-file <zip>. Report the returned findings rather than
assuming every rejection is a rollout issue. Runtime invocation and Store/Partner Center acceptance
remain separate checks. wiqd plugin export also fails explicitly for this connector kind
because Open Plugin has no verified representation for the ConnectionVault logical reference.
Publishing paths
Section titled “Publishing paths”wiqd carries you through build → validate → provision → package → publish or share. Choose the tenant path based on how users should receive the plugin:
- Organization app catalog —
wiqd plugin publish --env <env>submits the provisioned plugin to the tenant’s org admin catalog. Submission succeeds before approval; tenant users can discover the plugin only after an administrator approves it. - Direct access —
wiqd plugin share --scope users --email ...grants access to named users, orwiqd plugin share --scope tenantgrants tenant-wide access. Sharing is a different operation from catalog submission. - Public store — run
wiqd plugin package --env <env>and submit the.zipthrough Partner Center. This standalone-plugin lifecycle does not automate public-store submission. Declarative-agent packages have a separate direct Partner Center flow. Certification and store publication remain Partner Center responsibilities.
Normal publish validates the effective app manifest that it sends to fx-core:
appPackage/manifest.json by default, or the contained file selected with
--manifest. All DA manifests, skill folders, Worker references, and other
package files resolve from that selected manifest’s directory. A valid override
is not blocked by stale or invalid DA references in the unused default
manifest.
Before catalog submission, you can run a no-submit readiness check:
# Fully offline: project/path checks plus the applicable static DA phasewiqd plugin publish --dry-run --no-runtime --no-network --env dev
# Full applicable validation for a plugin that contains a declarative agentwiqd plugin publish --dry-run --env devDeclarative-agent checks are explicitly skipped for skill/connector-only plugins; a skip does not claim those components received full runtime validation.
Where wiqd stops: wiqd automates submission to the tenant organization app catalog, but tenant
administrator approval remains an admin action. For supported declarative-agent packages,
wiqd publish pc prepares, submits, and tracks your offer, while account verification,
certification, and store publication stay with Partner Center. See
Publish to Partner Center.
What’s Next?
Section titled “What’s Next?”- What is a plugin? — the mental model and the three meanings of “skill”
wiqd plugin— the full command reference- Plugin authoring reference — package anatomy,
SKILL.mdrules, connector requirements, validation codes, and the section-by-section Learn parity table - Quickstart — a shorter walkthrough for your first plugin
- Agent component lifecycle — agent-specific workflows and existing
wiqd agentprojects