Tasks and Built-In Tasks

This section documents MSBuild 18.9, the .NET SDK 10.0.402 (with its dotnet CLI) and NuGet 7.9, as published in the MSBuild, .NET and NuGet documentation on learn.microsoft.com, which is the reference these pages are written and verified against. Material about .NET 11 / NuGet 8.0 is still in preview and is always flagged as such — never presented as baseline.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The C# Reference bibliography lists the reference material consulted while preparing these pages.

Targets order the work; tasks do it. A task is a .NET class implementing Microsoft.Build.Framework.ITask; in a target it appears as an element named after the task, with its input parameters as attributes and <Output> children that capture its output parameters into properties or items. MSBuild ships more than sixty tasks — the compiler (Csc), file operations, Exec for running any command, MSBuild for building other projects — and the SDK and packages add more. This page shows the ones used most, with MSBuild 18.9 (.NET SDK 10.0.402); writing your own is covered on Inline Tasks and Writing Custom Tasks.

Calling a task

<Copy SourceFiles="@(Journal)" DestinationFolder="$(OutDir)journals" SkipUnchangedFiles="true">
  <Output TaskParameter="CopiedFiles" ItemName="Copied" />
</Copy>
  • Parameters map to the public settable properties of the task class. A parameter typed string takes a property or text; ITaskItem[] takes an item list (with metadata); bool takes true/false. A parameter marked [Required] must be set.

  • Output copies an output property of the task into an item list (ItemName) or a property (PropertyName).

  • Condition skips the task; ContinueOnError decides what a failure does: false/ErrorAndStop (default), true/WarnAndContinue, or ErrorAndContinue.

  • Before a task can be used it must be declared with UsingTask; the built-in tasks are declared in Microsoft.Common.tasks, which MSBuild loads for every project. Tasks in .OverrideTasks files next to MSBuild take precedence over all others.

A parameter typed as an item list cannot be built by concatenating text with an item list — MSBuild reports it at the point of the call:

<WriteLinesToFile File="$(OutDir)summary.txt" Lines="lines: @(JournalLine->Count())" Overwrite="true" />
~/tasks/tasks.proj(19,51): error MSB4012: The expression "lines: @(JournalLine->Count())" cannot be used in this context. Item lists cannot be concatenated with other strings where an item list is expected. Use a semicolon to separate multiple item lists.

Compute the value into a property first (<JournalLineCount>@(JournalLine->Count())</JournalLineCount> inside the target) and pass lines: $(JournalLineCount).

A tour of the built-in tasks

<Project DefaultTargets="Tour">
  <PropertyGroup>
    <OutDir>$(MSBuildProjectDirectory)/out/</OutDir>
  </PropertyGroup>
  <ItemGroup>
    <Journal Include="journals/*.ledger.csv" />
  </ItemGroup>

  <Target Name="Tour">
    <MakeDir Directories="$(OutDir)" />
    <Copy SourceFiles="@(Journal)" DestinationFolder="$(OutDir)journals" SkipUnchangedFiles="true">
      <Output TaskParameter="CopiedFiles" ItemName="Copied" />
    </Copy>
    <Message Importance="high" Text="Copied: @(Copied)" />

    <ReadLinesFromFile File="@(Journal)">
      <Output TaskParameter="Lines" ItemName="JournalLine" />
    </ReadLinesFromFile>
    <PropertyGroup>
      <JournalLineCount>@(JournalLine->Count())</JournalLineCount>
    </PropertyGroup>
    <WriteLinesToFile File="$(OutDir)summary.txt" Lines="lines: $(JournalLineCount)" Overwrite="true" />

    <XmlPeek XmlInputPath="ledger.config.xml" Query="/config/ledger/@currency">
      <Output TaskParameter="Result" PropertyName="Currency" />
    </XmlPeek>
    <Copy SourceFiles="ledger.config.xml" DestinationFolder="$(OutDir)" />
    <XmlPoke XmlInputPath="$(OutDir)ledger.config.xml" Query="/config/ledger/@currency" Value="USD" Condition="Exists('$(OutDir)ledger.config.xml')" />
    <Message Importance="high" Text="Currency from XmlPeek: $(Currency)" />

    <Exec Command="wc -l &lt; &quot;$(OutDir)journals/jan.ledger.csv&quot;" ConsoleToMSBuild="true" StandardOutputImportance="low">
      <Output TaskParameter="ConsoleOutput" PropertyName="LineCount" />
      <Output TaskParameter="ExitCode" PropertyName="ExecExit" />
    </Exec>
    <Message Importance="high" Text="Exec: wc printed '$(LineCount)', exit code $(ExecExit)" />

    <MSBuild Projects="child/child.proj" Targets="Describe" Properties="Region=EU">
      <Output TaskParameter="TargetOutputs" ItemName="ChildOutput" />
    </MSBuild>
    <Message Importance="high" Text="MSBuild task returned: @(ChildOutput)" />

    <CallTarget Targets="Archive" />
    <Warning Text="Ledger warning example" Code="LL1001" />
    <Error Text="never shown" Condition="'$(Currency)' == ''" />
  </Target>

  <Target Name="Archive">
    <ZipDirectory SourceDirectory="$(OutDir)journals" DestinationFile="$(OutDir)journals.zip" Overwrite="true" />
    <Delete Files="$(OutDir)summary.txt">
      <Output TaskParameter="DeletedFiles" ItemName="Deleted" />
    </Delete>
    <Message Importance="high" Text="Archived to journals.zip, deleted @(Deleted->'%(Filename)%(Extension)')" />
  </Target>
</Project>
dotnet msbuild tasks.proj -nologo -v:m
cat out/ledger.config.xml
  Copied: ~/tasks/out/journals/jan.ledger.csv
  Currency from XmlPeek: EUR
  Exec: wc printed '2', exit code 0
  MSBuild task returned: child built for EU
  Zipping directory "~/tasks/out/journals" to "~/tasks/out/journals.zip".
  Archived to journals.zip, deleted summary.txt
~/tasks/tasks.proj(43,5): warning LL1001: Ledger warning example
<config>
  <ledger currency="USD" />
</config>

child/child.proj has a single Describe target that returns an item built from $(Region).

Task Notes

Message, Warning, Error

Log text; Importance (high, normal, low) decides at which verbosity a message shows. Warning and Error take a Code, File, HelpKeyword/HelpLink, so custom diagnostics look like built-in ones (LL1001 above).

Exec

Runs a command through the shell (cmd.exe on Windows, /bin/sh elsewhere). ConsoleToMSBuild
ConsoleOutput capture the output; ExitCode returns it; IgnoreExitCode, IgnoreStandardErrorWarningFormat, CustomErrorRegularExpression, EchoOff, WorkingDirectory, EnvironmentVariables, StandardOutputImportance.

Copy

SourceFiles to DestinationFiles or DestinationFolder; SkipUnchangedFiles, Retries, RetryDelayMilliseconds, UseHardlinksIfPossible, UseSymboliclinksIfPossible; outputs CopiedFiles (all files it handled, copied or skipped) and DestinationFiles.

Delete, MakeDir, RemoveDir, Touch

File system; Delete outputs DeletedFiles, MakeDir outputs DirectoriesCreated.

ReadLinesFromFile, WriteLinesToFile

Lines as items; WriteLinesToFile has Overwrite, Encoding and WriteOnlyWhenDifferent (keeps the timestamp, so incremental targets that read the file are not invalidated).

XmlPeek, XmlPoke

Read or write with an XPath query (Query, Namespaces).

ZipDirectory, Unzip, DownloadFile

Archives and downloads (DownloadFile takes SourceUrl, DestinationFolder, Retries).

MSBuild

Builds other projects with Targets, Properties (global properties for them), RemoveProperties, BuildInParallel; TargetOutputs returns what their targets Returns. Per-project properties can be given as AdditionalProperties metadata on the Projects items.

CallTarget

Runs targets of the same project. Properties and items they create are not visible to the calling target until it ends — prefer DependsOnTargets.

When Exec fails

Exec fails the build when the command’s exit code is not zero, and also when the command prints something that looks like an MSBuild error (error XYZ: ...) unless IgnoreStandardErrorWarningFormat is set:

<Project>
  <Target Name="ExecFails">
    <Exec Command="grep -q 9999-Unknown journals/jan.ledger.csv" />
  </Target>
  <Target Name="ExecTolerated">
    <Exec Command="grep -q 9999-Unknown journals/jan.ledger.csv" IgnoreExitCode="true">
      <Output TaskParameter="ExitCode" PropertyName="GrepExit" />
    </Exec>
    <Message Importance="high" Text="grep exit code $(GrepExit) (ignored)" />
    <Exec Command="grep -q 9999-Unknown journals/jan.ledger.csv" ContinueOnError="WarnAndContinue" />
    <Message Importance="high" Text="still running after a failed Exec" />
  </Target>
</Project>
== ExecFails
~/tasks/errors.proj(3,5): error MSB3073: The command "grep -q 9999-Unknown journals/jan.ledger.csv" exited with code 1.
== ExecTolerated
  grep exit code 1 (ignored)
~/tasks/errors.proj(10,5): warning MSB3073: The command "grep -q 9999-Unknown journals/jan.ledger.csv" exited with code 1.
  still running after a failed Exec

The classic Exec bug is an unquoted path. The command is a single string passed to the shell, so a property that contains a space splits into two arguments:

<PropertyGroup>
  <DataDir>$(MSBuildProjectDirectory)/ledger data</DataDir>
</PropertyGroup>
<Target Name="Unquoted">
  <Exec Command="wc -l $(DataDir)/jan.ledger.csv" />
</Target>
<Target Name="Quoted">
  <Exec Command="wc -l &quot;$(DataDir)/jan.ledger.csv&quot;" />
</Target>
== Unquoted
  wc: ~/tasks/ledger: No such file or directory
  wc: data/jan.ledger.csv: No such file or directory
  0 total
~/tasks/quote.proj(6,5): error MSB3073: The command "wc -l ~/tasks/ledger data/jan.ledger.csv" exited with code 1.
== Quoted
  2 ~/tasks/ledger data/jan.ledger.csv

Write " around every path. When the command is longer than a line or platform-specific, a dedicated task (or a script file) is easier to get right than a quoted Exec.

When Copy fails

Copy can retry before giving up. The Copy task page says the task’s default is zero retries and that callers pass $(CopyRetryCount), but with MSBuild 18.9 a Copy with no Retries attribute retried 10 times, 1000 ms apart, and $(CopyRetryCount) was empty. Each failed attempt logs MSB3026, the last one MSB3027, and the failure itself MSB3021. Copying to a folder that cannot exist (/proc is a kernel file system) with two retries:

<Copy SourceFiles="journals/jan.ledger.csv" DestinationFolder="/proc/ledgerline" Retries="2" RetryDelayMilliseconds="100" />
~/tasks/errors.proj(14,5): warning MSB3026: Could not copy "journals/jan.ledger.csv" to "/proc/ledgerline/jan.ledger.csv". Beginning retry 1 in 100ms. Could not find file '/proc/ledgerline'.
~/tasks/errors.proj(14,5): warning MSB3026: Could not copy "journals/jan.ledger.csv" to "/proc/ledgerline/jan.ledger.csv". Beginning retry 2 in 100ms. Could not find file '/proc/ledgerline'.
~/tasks/errors.proj(14,5): error MSB3027: Could not copy "journals/jan.ledger.csv" to "/proc/ledgerline/jan.ledger.csv". Exceeded retry count of 2. Failed.
~/tasks/errors.proj(14,5): error MSB3021: Unable to copy file "journals/jan.ledger.csv" to "/proc/ledgerline/jan.ledger.csv". Could not find file '/proc/ledgerline'.

On Windows the usual cause is a locked file — the app or a test host is still running from bin/. The fix is to stop the process; the retries exist because antivirus scanners and indexers often hold a file for a moment. Change wave 18.7 made Copy retry on access-denied errors on non-Windows platforms too.

Common mistakes

  • Unquoted paths in Exec — see above.

  • Exec for things a task does portably (copy, delete, zip, XML edits) — the shell differs between Windows and Unix; the tasks do not.

  • Text concatenated with an item list in an ITaskItem[] parameter — MSB4012.

  • CallTarget to compute values for the calling target — they are not visible until the caller finishes.

  • ContinueOnError="true" everywhere — turns real failures into warnings; scope it to the one task that may fail.

  • WriteLinesToFile without WriteOnlyWhenDifferent in an incremental chain — a fresh timestamp invalidates every target that reads the file.