Built-in plugins

Four plugins are compiled into the extension. They need no plugins/ folder, no manifest of yours, and no install — uses: pwsh@1 works in a fresh workspace.

They are the only plugins that are not sandboxed. See the sandbox for why.

  1. shell@1
  2. pwsh@1
  3. file@1
  4. ai@1
    1. mcp: — reading systems there is no plugin for
    2. When to use mcp: instead of writing a plugin
    3. It enforces no gate
    4. One plugin, every vendor
  5. Why these four and no more

shell@1

Runs a command line through the OS shell. This is what a run: task desugars to, so you rarely write it out — but it is a plugin like any other, and its outputs are captured the same way.

Arg Type    
script string required The command line to run.
shell string pwsh | cmd | bash Which shell runs it. Absent means whatever the OS provides by default — cmd on Windows, bash elsewhere.

Outputs: stdout, stderr, exitCode.

tasks:
  list:
    uses: shell@1
    args:
      script: git status --short
    output: STATUS

pwsh must be named explicitly. A $env:NAME script with no shell: runs in cmd on Windows and comes back empty.


pwsh@1

Runs an inline PowerShell script, or a .ps1 file. PowerShell 7+.

Arg Type    
script string   Inline PowerShell. Mutually exclusive with file.
file string   Path to a .ps1, relative to the task’s working directory.
additionalArgs string   Arguments appended when running file, e.g. -Name "World".
shell string pwsh The only value accepted. A task that wants cmd wants a run: task.
errorActionPreference string default stop Prepended as $ErrorActionPreference.
progressPreference string default silentlyContinue Prepended as $ProgressPreference.
failOnStderr boolean default false Fail the task if anything reaches stderr, even on a zero exit.
ignoreExitCode boolean default false Report a non-zero exit without failing the task.

errorActionPreference and progressPreference both accept default, stop, continue and silentlyContinue.

Outputs: stdout, stderr, exitCode.

tasks:
  package:
    uses: pwsh@1
    args:
      file: ./scripts/package.ps1
      additionalArgs: -Configuration Release

errorActionPreference defaults to stop on purpose, and it is the single most load-bearing line this plugin emits. Without it a failing cmdlet is non-terminating: it writes to stderr, leaves $LASTEXITCODE alone, and the task reports success.

It governs cmdlets only. A native command exiting non-zero is unaffected, so git diff --quiet and friends still work as questions rather than failures.

progressPreference defaults to silentlyContinue so progress bars do not spray control characters into the log.

The script is staged as a UTF-8 .ps1 and invoked, rather than passed to pwsh -Command. So it is not capped by the command-line length limit, quotes survive intact, and it is not visible to anything that can list processes.

shell: cmd is rejected — a staged .ps1 is not something cmd.exe can run. A task that wants cmd wants a run: task, which already has shell:.


file@1

Reads a file into a run variable, or writes one from a value — the executes half of “AI proposes, a human approves, a plugin executes”.

Arg Type    
operation string requiredread | write | mkdir What to do with the path.
path string required Relative to the workspace unless absolute.
content string   What to write. Required when writing, ignored when reading.
createDirectories boolean default true Create parent folders when writing rather than failing on a missing one.

Outputs: content, path, lines.

tasks:
  read-notes:
    uses: file@1
    args:
      operation: read
      path: RELEASE_NOTES.md
    output: NOTES

  write-notes:
    needs: read-notes
    uses: file@1
    args:
      operation: write
      path: dist/notes.md
      content: ${{ run.NOTES.content }}

ai@1

Runs an agent session on whichever provider the task names. It reads, writes and works in the repo.

Put a gate on a later task if you want to review before anything downstream runs. This plugin changes your working tree.

It needs whatever its provider needs. ghcp — GitHub Copilot, and the only provider that ships today — uses the sign-in your editor already has. This is the only bundled plugin that needs anything at all.

Arg Type    
prompt string required What to ask the agent to do.
provider string default ghcp Which agent runtime the session runs on. The vendor is named here and nowhere else.
model string   Which model runs the session. Usually set once in a plugins: entry.
allow string[]   Tool patterns the session may use, e.g. ['builtin:*']. Unset means the provider’s usual toolset.
deny string[]   Tool patterns to withhold. Always wins over allow.
mcp object   MCP servers the session may call, keyed by name.
context object   Named documents laid into the session before the prompt, keyed by name — usually file args.
system string   Standing instructions for every turn, appended to the provider’s own system message.
attachments array   Files the session may open, as [{ path, name }]. Paths may sit outside the working directory.
detach boolean   Return the session id without waiting for the turn — for a conversation a person will join in the editor.
target string   The file this session is expected to produce. The path is told to the agent, and the task fails if the session ends without it.
title string   A short human title, sent as the prompt’s first line, so the chat history names the session.

Outputs: summary, files.

plugins:
  ai:
    uses: ai@1
    args:
      provider: ghcp
      model: claude-sonnet-4.5

tasks:
  draft:
    uses: ai
    args:
      title: Draft the release notes
      prompt: Summarise the commits since the last tag.
      context:
        changelog: { file: CHANGELOG.md }
    output: DRAFT

context pastes a document’s content into the session. attachments only tells the agent a file exists and lets it open it. Reach for context when the agent must read something, attachments when it might want to.

mcp: — reading systems there is no plugin for

tasks:
  pull-context:
    uses: ai@1
    args:
      mcp:
        ado:
          command: npx
          args: ["-y", "@azure-devops/mcp", "contoso"]
      allow: [wit_get_work_item]
      prompt: >
        Read work item 4821 and write docs/context/AB-4821.md -
        whatever structure serves a developer picking it up cold.

A remote server instead:

      mcp:
        ado:
          type: http
          url: https://mcp.dev.azure.com/contoso

Name the tools in allow:. Above 30 tools the runtime defers MCP definitions behind its tool-search tool rather than loading them all — so a large server does not cost a context window, but discovering a tool still costs a round trip. Naming it skips even that: one definition, one call, no search.

Never inline a credential. ${{ env.NAME }} resolves before the declaration is handed over, so a server’s env: can name a variable without the value ever being in the file.

When to use mcp: instead of writing a plugin

Both read a system like Azure DevOps, and they are not competing:

  Reach for
The same read every run, feeding a durable artefact a plugin
Exploratory — you don’t know what you’re looking for yet mcp:
Attachments — a .docx spec, a screenshot mcp:, clearly
It must run headless (schedule, CI, no Copilot sign-in) a plugin

The deciding difference is not token cost — a session pinned to one tool is cheap. It is that a plugin runs without a model at all, fails with a message naming the variable, and returns the same thing twice.

It enforces no gate

Not one. Every tool the agent takes is approved automatically, and the place a human decides anything is a trigger: manual task you put in the file — the same mechanism every other task uses.

  draft:
    uses: ai@1
    args:
      prompt: Write docs/specs/caching/requirements.md from the notes in docs/.
    output: SPEC

  implement:
    needs: draft
    uses: pwsh@1
    trigger: manual          # <- your call, not the plugin's
    args:
      file: ./scripts/implement.ps1

A permission card per tool was considered and rejected: nobody reads the fifth one, and a run recording “approved” against an unread card is worse than a run recording nothing. Use allow/deny to set policy once instead.

Reviewing is easier after the fact anyway. A 500-line spec cannot be read in a gate card; it can be read in your editor, which is where files tells you to look.

One plugin, every vendor

The vendor is provider:, not the plugin’s name. A second vendor is a second value, not a second plugin.json repeating this whole arg list to change one line — and a file that runs two of them declares two plugins: entries over the same ai@1.

There is deliberately no read-only preset. It would have to mean something specific per vendor, and a safety setting whose meaning changes with provider: is worse than none — so a session that must not touch the repository says exactly that in deny:, and the gate below it is what the workflow actually rests on.


Why these four and no more

There is no marketplace, nothing is downloaded, and nothing is signed. These four are what the engine needs to be useful on its own: a shell, a better shell, the filesystem, and an agent.

Anything past that is a plugin you write — which is a folder, a manifest, and a JavaScript file, with no build step and no dependency on this extension.


Back to top

Local Workflows is a VS Code extension. Everything it does is declared in a YAML file you own.