An issue that has come up in my professional life recently a few times has been how to take in bugs, features, etc from users external to a private Azure DevOps organization or project.  Azure DevOps work items are fantastic, but Azure Boards assume those creating and commenting have access to that AzDO Project and more over, to create, users have contributor access which requires a degree of licensing.

How can we ingest work or feedback akin to the JIRA Issue Collector or similar to  generic service now help desk forms?  

Today we will review a method utilizing a static form that leverages javascript to create and transmit a JSON payload to an Azure DevOps webhook.  This in turn will then process it into a work item on the users behalf.

Our Plan


First we we need to create a webhook:

I generally name the hook and the connection the same for simplicity:

Static Website

I started with this project : . It is a good example of a generated static app that can POST with a JSON payload. The key areas that need to be updated are app.js and the fields used in the form.

# from src/js/app.js

async function submitForm(e, form) {
   // 1. Prevent reloading page
   // 2. Submit the form
   // 2.1 User Interaction
   const btnSubmit = document.getElementById('btnSubmit');
   btnSubmit.disabled = true;
   setTimeout(() => btnSubmit.disabled = false, 2000);
   // 2.2 Build JSON body
   const jsonFormData = buildJsonFormData(form);
   // 2.3 Build Headers
   const headers = buildHeaders();
   // 2.4 Request & Response
   const response = await fetchService.performPostHttpRequest(``, headers, jsonFormData); // Uses JSON Placeholder
   // 2.5 Inform user of result
   if(response) {
       window.location.href = `/success.html`;
   } else {
       alert(`An error occured.`);

Also, in the js/service/FetchService.js we can stub out the processing of the response (since our example does not expect proper JSON back):

   async performPostHttpRequest(fetchLink, headers, body) {
       if(!fetchLink || !headers || !body) {
           throw new Error("One or more POST request parameters was not passed.");
       try {
           const rawResponse = await fetch(fetchLink, {
               method: "POST",
               headers: headers,
               body: JSON.stringify(body)
           // Webhooks are not JSON
           // const content = await rawResponse.json();
           //return content;
           return rawResponse;
       catch(err) {
           console.error(`Error at fetch POST: ${err}`);
           throw err;

index.html (which i renamed feedback.html):

 <div class="container card card-color">
       <form action="" id="sampleForm">
           <h2>Create a feedback task</h2>
           <div class="form-row">
               <label for="userId">Email Address</label>
               <input type="email" class="input-text input-text-block w-100" id="userId" name="userId" value="">
           <div class="form-row">
               <label for="summary">Summary</label>
               <input type="text" class="input-text input-text-block w-100" id="summary" name="summary">
           <div class="form-row">
               <label for="description">Description or Details</label>
               <textarea class="input-text input-text-block ta-100" id="description" name="description"></textarea>
           <div class="form-row mx-auto">
               <button type="submit" class="btn-submit" id="btnSubmit">
     <script src="js/app.js"></script>

I added one entry to the CSS for a larger textarea you can see referenced above

  .ta-100 {
    width: 100%;
    height: 200px;

Lastly, to really test we need to expose any HTML file, so update the "dev" line in the package.json

 "scripts": {
   "dev": "npm run clean && parcel src/*.html --out-dir dev feedback.html",

Setting up the pipeline

You can test the code above using npm run dev which will run a server on http://localhost:1234.

To setup build and release, I created a pipeline that could use 'npm run build' to build the "dist" folder:

- main
 vmImage: ubuntu-latest
- name: awsBucket
 value: freshbrewed-test
- name: awsFinalBucket
- name: awsCreds
 value: freshbrewed
- job: buildanddeploy
 displayName: "Build and Deploy"
 - task: NodeTool@0
     versionSpec: '10.*'
     checkLatest: true
 - script: |
     set -x
     npm install
     npm run build
     # copy to artifact staging
     cp -rf ./dist $(Build.ArtifactStagingDirectory)
   displayName: 'Check webhook payload'
 - task: PublishBuildArtifacts@1
     PathtoPublish: '$(Build.ArtifactStagingDirectory)'
     ArtifactName: 'drop'
     publishLocation: 'Container'
 - task:
   displayName: 'S3 Upload: ${{ variables.awsBucket }}'
     awsCredentials: ${{ variables.awsCreds }}
     regionName: 'us-east-1'
     bucketName: ${{ variables.awsBucket }}
     sourceFolder: '$(Build.ArtifactStagingDirectory)/dist'
     globExpressions: '**/*.*'
     filesAcl: 'public-read'
- job: waitforvalidation
 pool: server
 dependsOn: buildanddeploy
 timeoutInMinutes: 6440
 - task: ManualValidation@0
     notifyUsers: ''
     instructions: 'Please validate the build configuration and resume'
- job: deploytoprod
 dependsOn: waitforvalidation
 - task: DownloadBuildArtifacts@0
     buildType: 'current'
     downloadType: 'single'
     artifactName: 'drop'
     downloadPath: '$(System.ArtifactsDirectory)'
 - task:
   displayName: 'S3 Upload: ${{ variables.awsBucket }}'
     awsCredentials: ${{ variables.awsCreds }}
     regionName: 'us-east-1'
     bucketName: ${{ variables.awsFinalBucket }}
     sourceFolder: '$(System.ArtifactsDirectory)/drop/dist'
     globExpressions: '**/*.*'
     filesAcl: 'public-read'

There will be a manual intervention before it deploys to prod. Be aware if using the ManualValidation task that it must be tied to a serverless agent (like 'server').

Processing the webhook

We need a PAT just for making work items:

PAT for User

Note: PATs must have expiry so plan to need to renew later:

We can now save it into a new library:

Processing Pipeline

Let's now create a pipeline for processing the form

# Payload as sent by Web Form

    - webhook: feedbackForm
      connection: feedbackForm
  vmImage: ubuntu-latest

- group: AZDOAutomations

- script: |
    echo Add other tasks to build, test, and deploy your project.
    echo "userId: ${{ parameters.feedbackForm.userId }}"
    echo "summary: ${{ parameters.feedbackForm.summary }}"
    echo "description: ${{ parameters.feedbackForm.description }}"

    cat >rawDescription <<EOOOOL
    ${{ parameters.feedbackForm.description }}

    cat >rawSummary <<EOOOOT
    ${{ parameters.feedbackForm.summary }}

    cat rawDescription | sed ':a;N;$!ba;s/\n/<br\/>/g' | sed "s/'/\\\\'/g"> inputDescription
    cat rawSummary | sed ':a;N;$!ba;s/\n/ /g' | sed "s/'/\\\\'/g" > inputSummary
    echo "input summary: `cat inputSummary`"
    echo "input description: `cat inputDescription`"

    cat >$(Pipeline.Workspace)/ <<EOL
    set -x
    az boards work-item create --title '`cat inputSummary`' --type Feature --org --project ghost-blog --discussion 'requested by ${{ parameters.feedbackForm.userId }}' --fields System.Description='`cat inputDescription | tr -d '\n'`' > azresp.json
    chmod u+x

    echo ""
    cat $(Pipeline.Workspace)/

    echo "have a nice day."
  displayName: 'Check webhook payload'

- task: AzureCLI@2
  displayName: 'Create feature ticket'
    azureSubscription: 'My-Azure-SubNew'
    scriptType: 'bash'
    scriptLocation: 'scriptPath'
    scriptPath: '$(Pipeline.Workspace)/'


We can now trigger a pipeline by filling in the form and clicking submit:

Testing URL Web Form

which if deployed properly will reply with a success when submit is clicked

This then fires the pipeline tied to the webhook

and I immediately see an updated ticket

Satisfied all is good, i can go back and approve the manual intervention

Manual Validation Notice

clicking the link brings me to a pipeline for which i can reject or approve:

Actioning a Manual Validation

we can now see it deployed to production

and our form is live (go try it if you want):


Final Feedback Form

and we can submit a ticket

Results Page

which triggers the pipeline

which lastly creates a feature for me to consider going forward

I have in other cases setup Azure Logic Apps to trigger O365 emails. In our case, the only post notification i have presently set up is to slack:

And Datadog has an event alert setup on Production Builds


We used a simple static HTML form to create a JSON payload and transmit that to an Azure DevOps webhook.  We were able to test with a basic parcel server locally (http://localhost:1234).

With webhooks, we can test them locally by making a values.json file with the expected payload and hitting the webhook with curl.  e.g.

curl -v -X POST -d @values.json  -H "Content-type: application/json"

We used a Manual Intervention step to pause our pipeline between dev and prod sites and lastly, the already existing service connections triggered notifications on events.

This now allows feedback externally into a Private Azure DevOps project and we can use this a scalable solution for ingestion forms.