Plugin authoring reference
Plugin authoring reference
Section titled “Plugin authoring reference”This page is a reference, not a how-to — for the step-by-step walkthrough, see
Build a Plugin. It documents the constraints of the M365 app-package format
itself, and maps each one to where wiqd plugin surfaces it.
Package anatomy
Section titled “Package anatomy”The table below lists the files under the project’s appPackage/ directory — the same layout
wiqd plugin create scaffolds, with wiqd plugin add agent for an optional
declarative-agent component. Alongside it, at the project root, sit the lifecycle
file m365agents.yml and the per-environment env/.env.<env> files that
provision writes. WIQD derives project identity and capabilities from these
structural files and the root app manifest.
Path (under appPackage/) | Purpose |
|---|---|
manifest.json | The top-level Teams app manifest — declares the app and its components |
color.png | Full-color app icon, 192×192 |
outline.png | Transparent outline icon, 32×32 |
declarativeAgent.json | The declarative-agent component (if the plugin composes an agent) |
instruction.txt | The declarative agent’s instructions, referenced by declarativeAgent.json |
skills/<slug>/SKILL.md | A skill component — one folder per skill |
ai-plugin.json | An API-plugin manifest, at the package root — present only when a composed agent brings API-plugin actions[] |
apiSpecificationFile/ | The OpenAPI spec(s) backing those API-plugin actions |
A key divergence from hand-authoring: wiqd plugin create pins the manifest to
manifestVersion: "1.29" — a numbered, GA Teams schema — rather than hand-editing 1.28 or
devPreview. That version choice has a real behavioral consequence for connectors: at 1.29,
agentConnectors[] entries are URL-only and mcpToolDescription becomes optional; at 1.28,
mcpToolDescription is required, and if its referenced file isn’t included in the upload ZIP the
upload fails with an HTTP 400. Authoring against wiqd’s pinned 1.29 schema sidesteps that schema
failure — but not the platform one: Cowork requires mcpToolDescription regardless of what 1.29
permits, so a URL-only connector still fails to verify in the product. Supply one.
The numbered 1.29 pin remains the default. wiqd plugin add connector --dev-preview is an
explicit exception for referencing an existing OpenAPI custom connector or inferring remote MCP
ConnectionVault authorization: it updates both manifestVersion and $schema to the canonical
devPreview values. wiqd does not assume a tentative numbered manifest version supports either
preview capability. Explicit OAuth/API-key and anonymous remote MCP behavior is unchanged.
SKILL.md frontmatter rules
Section titled “SKILL.md frontmatter rules”Every skill’s SKILL.md starts with YAML frontmatter. Two fields are load-bearing:
| Field | Constraint |
|---|---|
name | 1–64 characters, kebab-case, must match its containing folder name exactly |
description | 1–1024 characters, should contain the trigger phrases that should surface the skill |
The single most common authoring mistake is a name that doesn’t match its folder — the two are
required to agree, and a mismatch is easy to introduce by renaming one without the other.
Kebab-case, precisely: lowercase alphanumeric characters and hyphens only. No underscores, no uppercase letters, no leading/trailing hyphens, and no consecutive hyphens.
name | Valid? | Why |
|---|---|---|
triage-issues | ✅ | lowercase, hyphen-separated |
triage-issues-v2 | ✅ | digits are fine after a hyphen |
Triage_Issues | ❌ | uppercase letters and an underscore |
-triage-issues | ❌ | leading hyphen |
triage--issues | ❌ | consecutive hyphens |
triage-issues- | ❌ | trailing hyphen |
Three-layer context-loading model
Section titled “Three-layer context-loading model”A skill’s content loads progressively, not all at once — this keeps a busy plugin’s context footprint small until a specific skill is actually needed:
- Frontmatter — loaded for every skill at startup, always. This is why
descriptionmust carry the trigger phrases: it’s the only part of an unused skill an agent has already seen. SKILL.mdbody — loaded only once a trigger phrase matches and the skill activates.references/— loaded on demand, only when the active skill’s instructions point at a specific reference file.
A fourth category, scripts/, is never loaded into context at all — scripts are executed, and
only their output (not their source) becomes part of the conversation.
Companion-file limits
Section titled “Companion-file limits”Agent Skills requires SKILL.md. references/, scripts/, and assets/ are organizational
conventions, not an exhaustive folder allowlist. Custom descendant directories such as
playbooks/ or workflows/, and companion files directly under the skill folder, are
format-valid. Host-specific restrictions still apply to their content.
Cowork plugin limits (manifest.json#agentSkills[])
Section titled “Cowork plugin limits (manifest.json#agentSkills[])”For a skill registered in the root Teams manifest’s agentSkills[], Microsoft 365 Copilot Cowork
allows at most 20 companion files per skill, 5 MB per companion file, and 10 MB total
across all companions. SKILL.md is mandatory and does not count as one of the 20 companion
files. Every other descendant file counts regardless of its directory name.
Every companion-file path must additionally be:
- Relative — no absolute paths.
- Free of
..path segments, backslashes, and null bytes. - Not a hidden file — no leading
.in the filename. - Not a Windows reserved name —
CON,PRN,AUX,NUL,COM1–COM9,LPT1–LPT9(with or without an extension) are all rejected. - Composed of safe filename characters only.
Declarative-agent limits (declarativeAgent.json#agent_skills[])
Section titled “Declarative-agent limits (declarativeAgent.json#agent_skills[])”A skill referenced only by a declarative agent’s declarativeAgent.json#agent_skills[] follows the
DA limits instead: at most 8 skills per agent, 350 files across all skills, and directory depth 3.
Agent Builder accepts files up to 25 MB and a skill ZIP up to 50 MB. Agents Toolkit uses a skill
directory and limits the complete application package to 10 MB. Cowork’s 20-companion limit does
not apply to a DA-only skill.
If the same folder is registered on both surfaces, it must satisfy both sets of constraints. These limits and distinctions were verified on October 14, 2026 against the Cowork plugin development documentation, the declarative-agent custom skills documentation, and the Agent Skills specification.
Discover and compose catalog connectors
Section titled “Discover and compose catalog connectors”Microsoft 365 catalog titles can publish complete agentConnectors nodes. Discover those
definitions directly instead of asking the user to manually provide an MCP URL or authorization
details. Discovery is the top-level wiqd connector command, which needs no
plugin project; composition is wiqd plugin add connector --selector:
wiqd connector list --search "Azure DevOps"wiqd connector show "Azure DevOps MCP"wiqd plugin add connector --selector "Azure DevOps MCP"Discovery uses the same Microsoft 365 identity as the core lifecycle commands and calls the titles catalog directly. It does not use Work IQ, Graph external connections, Power Platform environment enumeration, or a user-facing environment selector.
Keep the identities distinct:
| Identity | Example | Purpose |
|---|---|---|
| Selector | Azure DevOps MCP | Copyable input emitted by wiqd connector list |
| Catalog title ID | P_8d7e60b7-... | Internal source-package provenance |
| Connector element ID | 5947d764-... | agentConnectors[] element |
| Vault reference ID | shared_adomcpserver | Managed connector connection |
Connector type and connection answer different questions. Type describes how the agent calls
the tool: a Remote MCP endpoint or an existing OpenAPI custom connector. Connection describes how
the platform obtains access: managed connection, OAuth, API key, dynamic registration, or anonymous.
The same managed ConnectionVault mechanism can therefore appear on either connector type.
show returns the canonical node, including its toolSource. Composition with
wiqd plugin add connector --selector copies that exact validated node, preserves its returned
authorization definition, makes the capability visible from the root collection, and moves the app manifest to
devPreview. It does not acquire the title, register credentials, sign in to the connector,
create a connection, or mutate tenant state.
If the catalog node carries an inline tool list or references a title-owned file that is unavailable
in the target package, composition fails without writes and directs the author to materialize that
file through the manual connector path. --selector cannot be combined with the manual connector
options.
When the same MCP is published through multiple titles with equivalent tool and reuse definitions,
list deduplicates it and retains the friendly display-name selector. If materially different
connectors share a display name, list emits opaque composite selectors such as
P_abc...::6c00.... Copy the emitted name or composite selector directly into show or
--selector; users never need a separate title-ID option.
Catalog composition accepts remoteMcpServer nodes using ConnectionVault, OAuthPluginVault,
ApiKeyPluginVault, DynamicClientRegistration, or None. openApiConnector nodes are visible to
list and show, but catalog composition refuses them because runtime support is not yet broad.
After the user explicitly confirms the preview choice, use the result’s displayed referenceId
through the manual --kind openapi --dev-preview path below.
Connector requirements
Section titled “Connector requirements”wiqd plugin add connector supports two explicit kinds, plus --selector for reusing an exact
catalog connector:
| Kind | Inputs | Manifest shape |
|---|---|---|
remote-mcp (default) | HTTPS --url, optional tool description and MCP auth | toolSource.remoteMcpServer |
openapi | Existing custom-connector logical name in --auth-reference-id; explicit devPreview eligibility | toolSource.openApiConnector.authorization with type: "ConnectionVault" |
Remote MCP servers
Section titled “Remote MCP servers”An agent connector that fronts a remote MCP server must satisfy:
- HTTPS with TLS 1.2 or later.
- Streamable HTTP transport, speaking JSON-RPC 2.0.
- Support for both the
tools/listandtools/callmethods. - Each tool call completing in under 30 seconds.
wiqd’s divergence: wiqd plugin add connector writes a URL-only entry unless you pass
--tool-description, because the 1.29 schema marks mcpToolDescription optional.
Copilot Cowork treats
mcpToolDescriptionas required anyway. Its published connector validation rules list a missing one as an Error. Source validation does not check this connector content, and even a package validation pass does not prove runtime connectivity. A provisioned connector can still show “Could not verify connection” in the Cowork UI with nothing in the authoring loop having warned you. If you are targeting Cowork, capture the server’stools/listoutput to a file underappPackage/and pass--tool-description <file>.add connectorwarns when you leave it out.
The path is relative to appPackage/; a leading ./ is also accepted.
Include the referenced file in the ZIP to avoid a “not found in the app package” error.
add connector also enforces caps before writing, so an out-of-bounds entry never reaches the
manifest in the first place:
| Field | Cap |
|---|---|
| Connectors per manifest | ≤ 10 |
id (derived from --name) | ≤ 64 characters |
displayName (from --name) | ≤ 128 characters |
description (from --description) | ≤ 4000 characters |
mcpServerUrl (from --url) | ≤ 2048 characters |
ConnectionVault.referenceId (remote MCP reference or OpenAPI logical name) | ≤ 128 characters |
Existing OpenAPI custom connectors
Section titled “Existing OpenAPI custom connectors”Use this path only when the custom connector already exists:
wiqd plugin add connector --kind openapi --name "<Connector Name>" \ --description "<what the existing custom connector does>" \ --auth-reference-id "<logical-name>" --dev-previewThe command writes:
{ "toolSource": { "openApiConnector": { "authorization": { "type": "ConnectionVault", "referenceId": "<logical-name>" } } }}The reference is the custom connector’s logical name. It is not a URL, secret, connection
instance, OAuth registration id, OpenAPI file, or operation list. The platform resolves the
endpoint and OpenAPI description at runtime. URL, tool-description, and MCP --auth-type inputs
are rejected on this path.
This shape is currently devPreview-only. A fresh 1.29 project must use --dev-preview, which
updates both app-manifest version fields explicitly. A numbered version such as 1.30 or a tentative
future version is not treated as support without a confirmed platform contract. The same explicit
opt-in can repair those fields for an identical existing connector without duplicating it; the
command reports a manifest update, while a later unchanged invocation is a no-op. Package
validation is the relevant check; source validation does not inspect root connectors. The
CLI supports local devPreview packaging of OpenAPI
and remote MCP connectors using ConnectionVault, preserving their fields in the ZIP without
changing the source manifest. Local packaging does not establish whether server-side
AVL accepts the package. With a built ZIP, use wiqd plugin validate --mode package --package-file <zip> and inspect the actual server verdict. Do not rewrite or drop the connector
to bypass the local schema. Public schema docs, runtime invocation, and Store/Partner Center
acceptance are separate checks. Open Plugin export fails explicitly because that format has no verified
representation for the ConnectionVault logical reference.
Remote MCP auth types
Section titled “Remote MCP auth types”A remote MCP connector can declare these authentication types (API plugins have a separate contract):
None— no authentication.ConnectionVault— an existing connector reference, inferred when--auth-reference-idis supplied without--auth-type. DevPreview-only: use an already canonical preview manifest or opt in with--dev-preview. The command writes the supplied URL and reference; it does not create or look up the connector.OAuthPluginVault— OAuth, with tokens managed by the plugin vault.ApiKeyPluginVault— API-key authentication, with the key managed by the plugin vault. Note that API-key auth is not yet available in Cowork — use OAuth or Dynamic Client Registration (DCR) there instead.DynamicClientRegistration— the connector registers its own OAuth client at runtime instead of using a pre-configured client ID. This enum value is not how you enable DCR, and against Entra it does not work at all. Cowork’s own guidance is to omit theauthorizationnode (while still supplyingmcpToolDescription), which is what--auth-type dcremits. And Entra publishes no RFC 7591registration_endpoint, so DCR is unusable against any Entra-protected MCP server — including every first-party Microsoft one. If your server’s authorization server islogin.microsoftonline.com, skip DCR and register a client by hand.
There is deliberately no microsoftEntra type here. composeExtensions has one; agentConnectors
does not. The explicit OAuthPluginVault registration path needs a
client ID you own — including first-party Microsoft APIs. An existing ConnectionVault connector
reference does not require a new OAuth registration through this command. Because Entra publishes no RFC 7591
registration_endpoint, DCR is not a workaround for that: an Entra-protected server always costs a
hand-registered app.
wiqd plugin add connector wires the manifest side of this for you:
wiqd plugin add connector --name "GitHub MCP" --description "Authenticated GitHub access" \ --url https://api.githubcopilot.com/mcp/ \ --auth-type oauth --auth-reference-id <reference-id>To reference an existing remote MCP connector without registering another credential:
wiqd plugin add connector --kind remote-mcp --name "Contoso MCP" \ --description "Existing Contoso connector" --url "https://contoso.example.com/mcp" \ --auth-reference-id "shared_contoso" --dev-previewThis writes remoteMcpServer.authorization with type: "ConnectionVault" and
referenceId: "shared_contoso". --dev-preview explicitly updates both manifestVersion and
$schema to canonical DevPreview values. Without it, numbered or inconsistent manifests fail
before any writes; an already canonical DevPreview manifest needs no flag. An identical re-add
is unchanged, while explicit opt-in can repair noncanonical version/schema fields without
duplicating the connector. This uses the published DevPreview schema and does not establish
live schema deployment, runtime acceptance, or packaging readiness.
Open Plugin export refuses remote MCP ConnectionVault because the pinned exporter would
otherwise lose its reference. Explicit OAuth/API-key and anonymous connectors retain their
existing numbered-schema and export behavior.
Replace both the example URL and reference with the actual values for your connector.
The reference is trimmed, must be nonblank, and has a 128-character limit.
--auth-type accepts none, oauth, api-key, and dcr. With neither auth option, no
authorization node is written. With only --auth-reference-id, ConnectionVault is inferred.
Explicit oauth and api-key remain authoritative and require a credential reference;
explicit none and dcr write no authorization node and reject a reference.
For explicit OAuth/API-key authentication, register the credential first. The reference id comes from the Teams Developer Portal
(Tools → OAuth client registration) or from an oauth/register step in m365agents.yml.
add connector writes only that pointer — a client ID, client secret, or token must never appear
in the manifest. wiqd does not create the app registration or discover OAuth endpoints for you.
Validation codes
Section titled “Validation codes”wiqd plugin validate --mode source covers saved declarative-agent diagnostics and plugin project
guards, not full API/OpenAPI, skill, connector or app-manifest content. Everything below
surfaces at wiqd plugin validate --mode package or at upload/submission
time — knowing when each check runs tells you where in your workflow a given failure will
actually appear. The ASKILL-* codes are the Microsoft 365 Copilot platform’s own Agent Skills
validation codes (emitted by AVL and at upload), not wiqd-defined — wiqd surfaces them by running
package validation against that platform. --mode marketplace adds StoreOps policy/evidence
checks and uses the same package validation first. The default is package and uploads the ZIP;
choose source explicitly for offline checks. Old static/deep modes are rejected.
Manifest-level codes
Section titled “Manifest-level codes”These fire against the manifest’s agentSkills[] array itself, before the package is even
inspected — all are severity Error:
| Code | Meaning |
|---|---|
ASKILL-M001 | An agentSkills[] entry is missing its required folder property |
ASKILL-M002 | The agentSkills array declares more than 20 entries |
ASKILL-M003 | An agentSkills[] entry’s folder path is longer than 256 characters |
Package-level codes
Section titled “Package-level codes”These fire once the referenced skill folders are actually opened inside the ZIP — all are severity Error:
| Code | Meaning |
|---|---|
ASKILL-P001 | The folder an agentSkills[] entry points at doesn’t exist in the package |
ASKILL-P002 | That folder has no SKILL.md file |
ASKILL-P003 | SKILL.md’s frontmatter isn’t valid, ----delimited YAML |
ASKILL-P004 | SKILL.md frontmatter has no name field |
ASKILL-P005 | SKILL.md frontmatter has no description field |
ASKILL-P006 | SKILL.md’s name doesn’t match its containing folder’s name |
ASKILL-P007 | SKILL.md’s name isn’t valid kebab-case |
ASKILL-P008 | Two or more agentSkills[] entries point at the same folder |
Companion-file size and path violations aren’t part of this numbered set — they’re rejected under the rules in Companion-file limits instead.
Connector validation
Section titled “Connector validation”Connectors are validated as part of the same package-first pass, against a separate set of
platform rules rather than numbered ASKILL-* codes:
- Every connector needs both an
idand adisplayName, andids must be unique across the manifest’s connector list. - A connector must pick exactly one tool source —
plugin,remoteMcpServer,localMcpServer, oropenApiConnector— never multiple and never none. mcpServerUrlmust be a well-formed HTTPS URL.- On manifest 1.28, a
remoteMcpServerconnector must also carrymcpToolDescription, and the file it names must be present in the uploaded ZIP. wiqd’sadd connectortargets the pinned 1.29 schema and writes URL-only entries by default, so wiqd-authored connectors do not carry this field unless you pass--tool-description— see Package anatomy. When you do declare one,zipAppPackagecollects the referenced file into the app package. authorization.referenceIdis required whenever the auth type isn’tNone, and conversely must be absent when the type isNone.openApiConnector.authorization.typemust beConnectionVault, with a nonblank logicalreferenceIdno longer than 128 characters.
Where each surfaces
Section titled “Where each surfaces”| Failure class | Surfaces at |
|---|---|
| Saved declarative-agent schema, syntax and file-reference errors | wiqd plugin validate --mode source (offline shared native diagnostics) |
Full API/OpenAPI content, ASKILL-* codes and connector platform rules | wiqd plugin validate --mode package (Core/TDP), and at upload/submission |
Top-level manifest additionalProperties rejections (a field the Cowork plugin schema doesn’t define, under manifest 1.28) | wiqd plugin validate --mode package, and at upload/submission — a separate, unnumbered schema rejection, not an ASKILL-* code |
Cross-platform SKILL.md portability
Section titled “Cross-platform SKILL.md portability”SKILL.md follows the open Agent Skills standard, which is not wiqd- or Copilot-specific — the same
file format is understood by Claude Code, VS Code / GitHub Copilot, Gemini CLI, Cursor, JetBrains
Junie, OpenAI Codex, and other agent hosts. Authoring a skill once and exporting it (see
Import / export) is how you carry it to those other
tools without hand-translating the format.
Skill-authoring best practices
Section titled “Skill-authoring best practices”- Be specific about triggers. Phrase
descriptionas “Use when the user asks to…” rather than a generic capability summary — vague descriptions produce skills that either never activate or activate for the wrong requests. - Write numbered workflow steps that map to concrete, executable actions, not abstract guidance.
- Define an explicit output format so the agent’s response is predictable and testable.
- Reference connector tools by name. If the skill drives an MCP connector, name its tools explicitly rather than describing them vaguely — inspect the server’s advertised tool list before writing the instructions.
- Keep
SKILL.mdlean. Move detailed or rarely-needed material intoreferences/— remember the body loads on every trigger, but references load only on demand. - Never embed secrets in
SKILL.mdor any companion file. Use connector auth (see Remote MCP auth types) instead.
Learn-parity table
Section titled “Learn-parity table”Every section of the Microsoft Learn “Build plugins for Copilot Cowork” page, mapped to its wiqd equivalent:
| Learn section | Status |
|---|---|
| Package anatomy | Covered here — Package anatomy |
| Convert a Claude plugin | Covered — wiqd plugin import replaces Convert-ClaudePluginToMOS3.ps1 |
| Create a skill | Covered — Build a Plugin → Path 1 & 2, SKILL.md frontmatter rules |
| Reference materials | Covered — Three-layer context-loading model |
| Connector (MCP) | Covered — Remote MCP servers |
| Connector (existing OpenAPI custom connector) | Covered — Existing OpenAPI custom connectors |
| Manifest | Covered — Package anatomy |
| Icons | Covered — Package anatomy (192×192 / 32×32) |
| Package (zip with Compress-Archive) | Covered, different mechanism — wiqd plugin package replaces hand-zipping |
| Test / sideload | Covered, different mechanism — wiqd plugin provision + wiqd plugin share replace raw atk install sideloading |
| Publish to tenant | Covered — Build a Plugin → Publishing paths |
| Publish a standalone plugin to the public store | Use Partner Center’s portal; this plugin lifecycle does not submit it. Declarative-agent packages use the separate direct publishing flow. Certification remains external. |
| Test connector via dev tunnels | Out of scope for wiqd — wiqd does not manage dev tunnels; expose your MCP server yourself and pass its public https:// URL to wiqd plugin add connector |
| Packaging patterns | Covered — Package anatomy |
| Skill best practices | Covered — Skill-authoring best practices |
| Validation rules | Covered — Validation codes |
| Cross-platform (Claude, Cursor, etc.) | Covered — Cross-platform SKILL.md portability, wiqd plugin export |
| MCP annotations | Out of scope for wiqd — wiqd plugin add connector does not scaffold tool-level MCP annotations; the remote server owns them, or you declare them in a --tool-description file |
| Common questions | Covered — spread across this page and Build a Plugin; ask the conversational path (see Path 1) for anything not covered |
Go deeper
Section titled “Go deeper”- Build a Plugin — the step-by-step walkthrough (agentic and CLI paths)
- What is a plugin? — the mental model and the three meanings of “skill”
wiqd plugin— the full command referencewiqd plugin validate— source, package and Marketplace validation