Skip to content
Work IQ Dev Tools are in preview (v0.20.0). Commands and APIs may change before 1.0. See the installation guide.

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.

The table below lists the files under the project’s appPackage/ directory — the same layout wiqd agent scaffolds. Alongside it, at the project root, sit the wiqd descriptor wiqd.plugin.json, the ATK lifecycle file m365agents.yml, and the per-environment env/.env.<env> files that provision writes.

Path (under appPackage/)Purpose
manifest.jsonThe top-level Teams app manifest — declares the app and its components
color.pngFull-color app icon, 192×192
outline.pngTransparent outline icon, 32×32
declarativeAgent.jsonThe declarative-agent component (if the plugin composes an agent)
instruction.txtThe declarative agent’s instructions, referenced by declarativeAgent.json
skills/<slug>/SKILL.mdA skill component — one folder per skill
ai-plugin.jsonAn 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 --kind openapi --dev-preview is an explicit exception for referencing an existing OpenAPI custom connector: it updates both manifestVersion and $schema to the canonical devPreview values. wiqd does not assume a tentative numbered manifest version supports that connector.

Every skill’s SKILL.md starts with YAML frontmatter. Two fields are load-bearing:

FieldConstraint
name1–64 characters, kebab-case, must match its containing folder name exactly
description1–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.

nameValid?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

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:

  1. Frontmatter — loaded for every skill at startup, always. This is why description must carry the trigger phrases: it’s the only part of an unused skill an agent has already seen.
  2. SKILL.md body — loaded only once a trigger phrase matches and the skill activates.
  3. 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.

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.

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:

Terminal window
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:

IdentityExamplePurpose
SelectorAzure DevOps MCPCopyable input emitted by wiqd connector list
Catalog title IDP_8d7e60b7-...Internal source-package provenance
Connector element ID5947d764-...agentConnectors[] element
Vault reference IDshared_adomcpserverManaged 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, updates the plugin capability cache, 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.

wiqd plugin add connector supports two explicit kinds, plus --selector for reusing an exact catalog connector:

KindInputsManifest shape
remote-mcp (default)HTTPS --url, optional tool description and MCP authtoolSource.remoteMcpServer
openapiExisting custom-connector logical name in --auth-reference-id; explicit devPreview eligibilitytoolSource.openApiConnector.authorization with type: "ConnectionVault"

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/list and tools/call methods.
  • 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 mcpToolDescription as required anyway. Its published connector validation rules list a missing one as an Error, and a URL-only connector will pass validate, pass validate --mode deep, provision cleanly, and then 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’s tools/list output to a file under appPackage/ and pass --tool-description <file>. add connector warns when you leave it out.

The path is relative to appPackage/. A leading ./ — the form Microsoft Learn’s own examples use — is accepted and canonicalized away, because validateAppPackage compares the raw manifest string against ZIP entry names (which carry no prefix) and would otherwise reject it with “not found in the app package.”

add connector also enforces caps before writing, so an out-of-bounds entry never reaches the manifest in the first place:

FieldCap
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 (OpenAPI logical name)≤ 128 characters

Use this path only when the custom connector already exists:

Terminal window
wiqd plugin add connector --kind openapi --name "<Connector Name>" \
--description "<what the existing custom connector does>" \
--auth-reference-id "<logical-name>" --dev-preview

The 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-first deep validation is the relevant check; static validation does not inspect root connectors. The bundled fx-core 3.1.5-alpha.36e30da2f6.0 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 deep --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.

A remote MCP connector or API plugin can declare one of these authentication types:

  • None — no authentication.
  • 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 the authorization node (while still supplying mcpToolDescription), which is what --auth-type dcr emits. And Entra publishes no RFC 7591 registration_endpoint, so DCR is unusable against any Entra-protected MCP server — including every first-party Microsoft one. If your server’s authorization server is login.microsoftonline.com, skip DCR and register a client by hand.

There is deliberately no microsoftEntra type here. composeExtensions has one; agentConnectors does not, so there is no SSO path for a connector. Every OAuth-protected MCP server therefore needs a client ID you own — including first-party Microsoft APIs. And 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:

Terminal window
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>

--auth-type accepts none (default), oauth, api-key, and dcr. oauth and api-key require --auth-reference-id; none and dcr write no authorization node and reject one.

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.

Static wiqd plugin validate covers only the declarative-agent (MVL) surface. Everything below surfaces only at wiqd plugin validate --mode deep (package-first AVL) or at upload/submission time — knowing which layer owns which code 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 deep validation against that platform.

These fire against the manifest’s agentSkills[] array itself, before the package is even inspected — all are severity Error:

CodeMeaning
ASKILL-M001An agentSkills[] entry is missing its required folder property
ASKILL-M002The agentSkills array declares more than 20 entries
ASKILL-M003An agentSkills[] entry’s folder path is longer than 256 characters

These fire once the referenced skill folders are actually opened inside the ZIP — all are severity Error:

CodeMeaning
ASKILL-P001The folder an agentSkills[] entry points at doesn’t exist in the package
ASKILL-P002That folder has no SKILL.md file
ASKILL-P003SKILL.md’s frontmatter isn’t valid, ----delimited YAML
ASKILL-P004SKILL.md frontmatter has no name field
ASKILL-P005SKILL.md frontmatter has no description field
ASKILL-P006SKILL.md’s name doesn’t match its containing folder’s name
ASKILL-P007SKILL.md’s name isn’t valid kebab-case
ASKILL-P008Two 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.

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 id and a displayName, and ids must be unique across the manifest’s connector list.
  • A connector must pick exactly one tool source — plugin, remoteMcpServer, localMcpServer, or openApiConnector — never multiple and never none.
  • mcpServerUrl must be a well-formed HTTPS URL.
  • On manifest 1.28, a remoteMcpServer connector must also carry mcpToolDescription, and the file it names must be present in the uploaded ZIP. wiqd’s add connector targets 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, zipAppPackage collects the referenced file into the app package.
  • authorization.referenceId is required whenever the auth type isn’t None, and conversely must be absent when the type is None.
  • openApiConnector.authorization.type must be ConnectionVault, with a nonblank logical referenceId no longer than 128 characters.
Failure classSurfaces at
Declarative-agent manifest / API-plugin manifest / OpenAPI errorswiqd plugin validate (static, MVL)
ASKILL-* codes, connector validation ruleswiqd plugin validate --mode deep (package-first, AVL), 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 deep, and at upload/submission — a separate, unnumbered schema rejection, not an ASKILL-* code

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.

  • Be specific about triggers. Phrase description as “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.md lean. Move detailed or rarely-needed material into references/ — remember the body loads on every trigger, but references load only on demand.
  • Never embed secrets in SKILL.md or any companion file. Use connector auth (see Remote MCP auth types) instead.

Every section of the Microsoft Learn “Build plugins for Copilot Cowork” page, mapped to its wiqd equivalent:

Learn sectionStatus
Package anatomyCovered here — Package anatomy
Convert a Claude pluginCovered — wiqd plugin import replaces Convert-ClaudePluginToMOS3.ps1
Create a skillCovered — Build a Plugin → Path 1 & 2, SKILL.md frontmatter rules
Reference materialsCovered — Three-layer context-loading model
Connector (MCP)Covered — Remote MCP servers
Connector (existing OpenAPI custom connector)Covered — Existing OpenAPI custom connectors
ManifestCovered — Package anatomy
IconsCovered — Package anatomy (192×192 / 32×32)
Package (zip with Compress-Archive)Covered, different mechanism — wiqd plugin package replaces hand-zipping
Test / sideloadCovered, different mechanism — wiqd plugin provision + wiqd plugin share replace raw atk install sideloading
Publish to tenantCovered — Build a Plugin → Publishing paths
Publish to the public storeOut of scope for wiqd — Partner Center certification/submission is a human/admin step outside wiqd; see Publishing paths
Test connector via dev tunnelsOut 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 patternsCovered — Package anatomy
Skill best practicesCovered — Skill-authoring best practices
Validation rulesCovered — Validation codes
Cross-platform (Claude, Cursor, etc.)Covered — Cross-platform SKILL.md portability, wiqd plugin export
MCP annotationsOut 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 questionsCovered — spread across this page and Build a Plugin; ask the conversational path (see Path 1) for anything not covered