Table of Contents
Azure DevOps Extension: Send email through Graph
Summary: How to get started with the Azure DevOps Extension for sending email through Microsoft Graph.
Date: 9 December 2025
This is a support page for the Azure DevOps Extension: Send email through Graph.
Marketplace link: https://marketplace.visualstudio.com/items?itemName=GetShifting.GraphEmail
GitHub Repository: https://github.com/getshifting/getshifting/tree/main/adoExtensionGraphEmail
Overview
The marketplace extension page provides a brief overview on how to use the extension, and how to configure the requirements. This page provides more examlples and also some background on how the extension creation is done and tested.
Yaml Examples
The examples below are also provided in the marketplace extension page:
This is an example of a task to send a multiline email:
- task: GetShifting.GraphEmail.graph-email-build-task.GraphEmail@0 displayName: "Send an email with subject Test mail from $(BUILD.DEFINITIONNAME)" inputs: To: "sjoerd@getshifting.com" BCC: "$(BUILD.REQUESTEDFOREMAIL)" From: "sjoerd@getshifting.com" Subject: "Test mail from $(BUILD.DEFINITIONNAME)" Body: | <h1>This is a testmail</h1> <p>You can use various variables within the body of the email, for example:</p> <ul> <li> Build ID: $(build.buildid) </li> <li> Build Directory: $(agent.builddirectory) </li> <li> Build Queued By: $(BUILD.QUEUEDBY) </li> <li> Agent Name: $(AGENT.NAME) </li> <li> Job Name: $(SYSTEM.JOBDISPLAYNAME) </li> <li> Task Name: $(SYSTEM.TASKDISPLAYNAME) </li> <li> Build Requested for: $(BUILD.REQUESTEDFOR) </li> <li> Commit Message: $(BUILD.SOURCEVERSIONMESSAGE) </li> </ul> Kind regards, <br> $(BUILD.REQUESTEDFOR) ClientID: "e5e6ce84-d241-4faf-97e0-d71a171f1adf" ClientSecret: "$(ClientSecret)" ShowClientSecret: false TenantDomain: getshifting.com
This is an example of a task to send a single line email:
- task: GetShifting.GraphEmail.graph-email-build-task.GraphEmail@0 displayName: "Send graph email with subject Testmail from $(BUILD.DEFINITIONNAME)" inputs: To: "sjoerd@getshifting.com" From: "sjoerd@getshifting.com" Subject: "Testmail from $(BUILD.DEFINITIONNAME)" Body: This is a short testmail ClientID: "$(ClientID)" ClientSecret: "$(ClientSecret)" TenantDomain: getshifting.com
Classic Pipeline Example
Even though classic pipelines are not used that much anymore, when I originally created the extension I still mostly used classic pipelines. Below is an example I used in my pipeline to test the extension:
The Build and Release Pipeline
Below is the almost full build and release pipeline I use to build, test and release the extension to the marketplace. Some notes have been removed, and some of the guids are changed. The tokenized files that are used in the replacetokens task can be reviewed on the public repository of the extension: https://github.com/getshifting/getshifting/tree/main/adoExtensionGraphEmail:
name: $(Build.DefinitionName)-$(Build.BuildId) appendCommitMessageToRunName: false variables: - group: ExtensionGraph - name: ClientID value: "42ff8d60-02e0-43dd-9b57-008cbdd86dd2" - name: tfxcli value: "v0.9.x" - name: extensionid value: "graphemail" - name: publisherprod value: "getshifting" - name: publisher value: "getshifting-private" - name: public value: "false" - name: currentDate value: $[format('{0:yyyyMMdd}', pipeline.startTime)] parameters: - name: action displayName: Action type: string default: "Build and Release Privately only" values: - "Build Only" - "Build and Release Privately only" - "Build and Release Public" pool: vmImage: windows-latest trigger: none resources: repositories: - repository: self stages: - stage: build displayName: "Stage: Build" jobs: - job: build displayName: "Job: Build & Package" steps: - task: PowerShell@2 displayName: "Get System Variables" condition: eq(variables['System.debug'], true) inputs: pwsh: true targetType: "inline" script: | Write-Host "`n##[section]Get System Variables`n" Get-ChildItem -path env:* | Sort-Object Name - task: ms-devlabs.vsts-developer-tools-build-tasks.tfx-installer-build-task.TfxInstaller@5 displayName: "Use Node CLI for Azure DevOps (tfx-cli): $(tfxcli)" inputs: version: "$(tfxcli)" - task: qetza.replacetokens.replacetokens-task.replacetokens@6 displayName: 'Replace tokens in vss-extension.json graphEmail\task.json' inputs: sources: | vss-extension.json graphEmail\task.json tokenPattern: doubleunderscores telemetryOptout: true root: '$(System.DefaultWorkingDirectory)\public\adoExtensionGraphEmail' verbosity: "debug" - task: PowerShell@2 displayName: "Add VstsTaskSdk PowerShell Module to the extension package" inputs: pwsh: true targetType: "inline" workingDirectory: '$(System.DefaultWorkingDirectory)\public\adoExtensionGraphEmail' script: | # Set the verbose flag based on pipeline variable if ($env:SYSTEM_DEBUG -eq "True"){ Write-Host "##[debug]Verbose logging is enabled" $verboseFlag = $true } else { $verboseFlag = $false } Write-Host "##[section]Set Variables" $vstsTaskSdkDir = ".\graphEmail\ps_modules\VstsTaskSdk" $vstsTempDir = ".\vststemp" Write-Host "VstsTaskSdk Directory : $vstsTaskSdkDir" Write-Host "VstsTaskSdk Temp Directory: $vstsTempDir" if ($verboseFlag) { Write-Host "##[debug]Check current directory structure" Get-Location Get-ChildItem -Recurse } Write-Host "##[section]Save VstsTaskSdk to $vstsTempDir and copy to $vstsTaskSdkDir" Write-Host "`nCreate required directory structure" New-Item -Path $vstsTaskSdkDir -ItemType "Directory" -Verbose:$verboseFlag New-Item -Path $vstsTempDir -ItemType "Directory" -Verbose:$verboseFlag Write-Host "`nSave VstsTaskSdk Module to temporary directory" Save-Module -Name VstsTaskSdk -Path $vstsTempDir -Verbose:$verboseFlag Write-Host "`nCopy all required files to VstsTaskSdk directory" Get-ChildItem -Path $vstsTempDir -Recurse -File -Depth 2 | ForEach-Object { Copy-Item -Path $_.FullName -Destination $vstsTaskSdkDir -Verbose:$verboseFlag } if ($verboseFlag) { Write-Host "##[debug]Check current directory structure" Get-Location Get-ChildItem -Recurse } Write-Host "##[section]Remove temporary directory" Remove-Item -Path $vstsTempDir -Recurse -Force -Verbose:$verboseFlag if ($verboseFlag) { Write-Host "##[debug]Check current directory structure" Get-Location Get-ChildItem -Recurse } - task: ms-devlabs.vsts-developer-tools-build-tasks.package-extension-build-task.PackageAzureDevOpsExtension@5 displayName: "Package Extension" inputs: rootFolder: "$(Build.SourcesDirectory)/public/adoExtensionGraphEmail" outputPath: "$(build.artifactstagingdirectory)" extensionVersion: "$(currentDate).$(Build.BuildId).$(System.StageAttempt)" - task: PublishBuildArtifacts@1 displayName: "Publish Artifact: extension" inputs: ArtifactName: extension - stage: releasePrivate displayName: "Stage: Release Private" condition: and(succeeded(), or( contains('${{ parameters.action }}', 'Build and Release Privately only'), contains('${{ parameters.action }}', 'Build and Release Public'))) jobs: - job: releasePrivate displayName: "Job: Release Privately" steps: - task: ms-devlabs.vsts-developer-tools-build-tasks.tfx-installer-build-task.TfxInstaller@5 displayName: "Use Node CLI for Azure DevOps (tfx-cli): $(tfxcli)" inputs: version: "$(tfxcli)" - task: DownloadPipelineArtifact@2 inputs: artifactName: "extension" targetPath: "$(System.DefaultWorkingDirectory)/DevOpsExtensionGraph/extension" - task: ms-devlabs.vsts-developer-tools-build-tasks.publish-extension-build-task.PublishAzureDevOpsExtension@5 displayName: "Publish Extension" inputs: connectTo: "VsTeam" connectedServiceName: "AzureDevOpsMarketPlace" fileType: vsix vsixFile: "$(System.DefaultWorkingDirectory)/DevOpsExtensionGraph/extension/getshifting-private.GraphEmail-$(currentDate).$(Build.BuildId).$(System.StageAttempt).vsix" updateTasksVersion: false extensionVisibility: private extensionPricing: free - job: waitForInstall displayName: "Job: Wait for the Private Extension to be installed" dependsOn: releasePrivate pool: server timeoutInMinutes: 1440 steps: - task: ManualValidation@0 displayName: "Wait for the private extension is installed" timeoutInMinutes: 120 inputs: notifyUsers: | sjoerd@getshifting.com instructions: 'Go to the Marketplace icon in the top right, and select "Manage Extensions". Click on "Send email through Graph by GetShifting-Private" and wait for the extension to be automatically updated to the just released version.' onTimeout: "reject" - job: testPublic displayName: "Job: Test Privately" dependsOn: waitForInstall steps: - task: GetShifting-Private.GraphEmail.graph-email-build-task.GraphEmail@0 displayName: "Send an email with subject Test mail from $(BUILD.DEFINITIONNAME)" inputs: To: "sjoerd@getshifting.com" BCC: "$(BUILD.REQUESTEDFOREMAIL)" From: "sjoerd@getshifting.com" Subject: "Test mail from $(BUILD.DEFINITIONNAME)" Body: | <h1>This is a testmail</h1> <p>You can use various variables within the body of the email, for example:</p> <ul> <li> Build ID: $(build.buildid) </li> <li> Build Directory: $(agent.builddirectory) </li> <li> Build Queued By: $(BUILD.QUEUEDBY) </li> <li> Agent Name: $(AGENT.NAME) </li> <li> Job Name: $(SYSTEM.JOBDISPLAYNAME) </li> <li> Task Name: $(SYSTEM.TASKDISPLAYNAME) </li> <li> Build Requested for: $(BUILD.REQUESTEDFOR) </li> <li> Commit Message: $(BUILD.SOURCEVERSIONMESSAGE) </li> </ul> Kind regards, <br> $(BUILD.REQUESTEDFOR) ClientID: "$(ClientID)" ClientSecret: "$(ClientSecret)" ShowClientSecret: true TenantDomain: getshifting.com - stage: releasePublic displayName: "Stage: Release Public" condition: and(succeeded(), eq('${{ parameters.action }}', 'Build and Release Public')) jobs: - job: releasePublic displayName: "Job: Release Publicly" steps: - task: ms-devlabs.vsts-developer-tools-build-tasks.tfx-installer-build-task.TfxInstaller@5 displayName: "Use Node CLI for Azure DevOps (tfx-cli): $(tfxcli)" inputs: version: "$(tfxcli)" - task: DownloadPipelineArtifact@2 inputs: artifactName: "extension" targetPath: "$(System.DefaultWorkingDirectory)/DevOpsExtensionGraph/extension" - task: ms-devlabs.vsts-developer-tools-build-tasks.publish-extension-build-task.PublishAzureDevOpsExtension@5 displayName: "Publish Extension" inputs: connectTo: "VsTeam" connectedServiceName: "AzureDevOpsMarketPlace" fileType: vsix vsixFile: "$(System.DefaultWorkingDirectory)/DevOpsExtensionGraph/extension/getshifting-private.GraphEmail-$(currentDate).$(Build.BuildId).$(System.StageAttempt).vsix" publisherId: "$(publisherprod)" updateTasksVersion: false updateTasksId: true extensionVisibility: public extensionPricing: free - job: waitForInstall displayName: "Job: Wait for the Public Extension to be installed" dependsOn: releasePublic pool: server timeoutInMinutes: 1440 steps: - task: ManualValidation@0 displayName: "Wait for the public extension is installed" timeoutInMinutes: 120 inputs: notifyUsers: | sjoerd@getshifting.com instructions: 'Go to the Marketplace icon in the top right, and select "Manage Extensions". Click on "Send email through Graph by GetShifting" and wait for the extension to be automatically updated to the just released version.' onTimeout: "reject" - job: testPublic displayName: "Job: Test Publicly" dependsOn: waitForInstall steps: - task: GetShifting.GraphEmail.graph-email-build-task.GraphEmail@0 displayName: "Send graph email with subject Testmail from $(BUILD.DEFINITIONNAME)" inputs: To: "sjoerd@getshifting.com" From: "sjoerd@getshifting.com" Subject: "Testmail from $(BUILD.DEFINITIONNAME)" Body: Test from public ClientID: "$(ClientID)" ClientSecret: "$(ClientSecret)" TenantDomain: getshifting.com

