A customer submits an RFP document to a shared SharePoint library. An Azure Function picks it up, Azure Content Understanding in Foundry Tools extracts its text and layout, deterministic routing rules identify the required capabilities, and a summary card is posted to a Microsoft Teams channel.
This .NET sample shows how to implement the scenario above using the Azure Functions Connector extension and the Azure Connectors .NET SDK. It uses two connections, SharePoint Online and Microsoft Teams, created in an Azure Connector Namespace, and leverages the function app's managed identity for authentication.
- Azure Developer CLI (
azd) - Azure CLI (
az) β₯ 2.75.0 - Azure Functions Core Tools for local development.
- .NET 10 SDK
- Azurite for local development.
jq(macOS and Linux only)connector-namespaceAzure CLI extension- Visual Studio Code for local development with a dev tunnel.
- A SharePoint site + document library to receive RFPs.
- RFPs with a
Customer,Client, orOrganizationfield and a numberedRequired Capabilitiessection. The included sample demonstrates the expected structure. - A Microsoft Teams team and standard or private channel to post to. The deployed end-to-end workflow has been verified with a private channel. The Teams Workflows app must be allowed in the Teams admin center (required by the card-posting action). See the Microsoft Teams connector documentation for details.
-
Clone the repo:
git clone https://github.com/Azure-Samples/functions-connectors-net-rfp-intake-sharepoint-teams.git cd functions-connectors-net-rfp-intake-sharepoint-teams
-
Log in to Azure:
azd auth login az login
-
Inside the root directory, create an
azdenvironment. Its name is used to derive the resource group and resource names:azd env new rfp-demo -
If you don't know the Teams IDs, list the teams you have joined:
az rest --method get --url "https://graph.microsoft.com/v1.0/me/joinedTeams" --query "value[].{name:displayName,teamId:id}" -o table
Then use the team ID to list its channels:
az rest --method get --url "https://graph.microsoft.com/v1.0/teams/<team-id>/channels" --query "value[].{name:displayName,channelId:id}" -o table
-
Provision the resources, deploy the Function App, authorize both connector connections, and configure the SharePoint trigger:
azd up
During the command, you are prompted for:
- Azure Subscription (
00000000-0000-0000-0000-000000000000): subscription where the resources will be provisioned. - Location (
East US 2): Azure region where resources will be deployed. The deployment limits this prompt to regions supported by Content Understanding. SHAREPOINT_SITE_URL(https://contoso.sharepoint.com/sites/RFPs): SharePoint site that contains the document library to monitor.SHAREPOINT_LIBRARY_NAME(Documents): SharePoint document library that contains the RFP files. This is not a folder name.TEAMS_TEAM_ID(00000000-0000-0000-0000-000000000000): Microsoft 365 group ID of the team that receives the summary card.TEAMS_CHANNEL_ID(19:example-channel-id@thread.tacv2): channel ID within the team that receives the summary card.
The platform-specific
authorize-connectionsscript opens a browser for the SharePoint and Teams OAuth consent flows. On each page, select I have verified this request and trust the source, then select Allow access. Connections that are already authenticated are skipped. The post-provision hook then generateslocal.settings.jsonfor optional local development. - Azure Subscription (
-
Upload
sample-data/contoso-rfp.pdfto the monitored SharePoint library. Use a new file name if the sample was uploaded previously because the trigger listens for newly created files. -
Allow up to five minutes for the SharePoint trigger to detect the file. A "New RFP received" Adaptive Card appears in your Teams channel:
π New RFP received Customer: Contoso Ltd. Source file: contoso-rfp.pdf Required capabilities - Azure AI - Data Platform - Identity & Security - Integration & Automation - Observability & Operations Recommended SMEs - AI Specialist - Data Platform Engineer - Security Architect - Integration Architect - Cloud Operations SpecialistThe
prebuilt-layoutanalyzer supports PDF, image, Microsoft Office, HTML, email, and text-based documents. Content Understanding performs OCR and layout extraction; the application then uses explicit, testable rules to parse the customer and numbered capability headings and map them to SME roles. This analyzer does not require a language or embedding model.
Run the offline parser and SharePoint-content decoding tests:
dotnet test tests/RfpApp.Tests/RfpApp.Tests.csproj --filter "Category!=Integration"To verify the included PDF against the provisioned Microsoft Foundry resource,
sign in with az login. azd provision grants the provisioning identity the
Cognitive Services User role.
PowerShell:
$env:CONTENT_UNDERSTANDING_ENDPOINT = azd env get-value contentUnderstandingEndpoint
dotnet test tests/RfpApp.Tests/RfpApp.Tests.csproj --filter "Category=Integration"macOS or Linux:
export CONTENT_UNDERSTANDING_ENDPOINT="$(azd env get-value contentUnderstandingEndpoint)"
dotnet test tests/RfpApp.Tests/RfpApp.Tests.csproj --filter "Category=Integration"If you test against a different account, set its endpoint manually and grant
your identity the Cognitive Services User role first.
Local execution still uses the connector connections and Content Understanding
resource provisioned in Azure. If you haven't run azd up, run azd provision
and complete both connector consent flows first.
-
The post-provision hook creates
local.settings.jsonfrom the current azd deployment outputs, filling in the connector runtime URLs, SharePoint site, Teams destination, and Content Understanding endpoint. It leavesAZURE_CLIENT_IDempty soDefaultAzureCredentialuses your local Azure sign-in instead of the Function App's managed identity.To regenerate the file after changing environments or provisioning values, run:
pwsh ./infra/scripts/createlocalsettings.ps1 -Force
On macOS or Linux:
sh ./infra/scripts/createlocalsettings.sh --force
-
Start Azurite in a separate terminal:
azurite --silent --location ~/.azurite/connectors-sample
-
Start the app with authentication enabled:
func start --enableAuthAlways use
--enableAuthwhen exposing the app through a dev tunnel. Without it, the function endpoint is unauthenticated on the public internet. -
In VS Code, open the Ports view, select Forward a Port, and enter
7071. -
Copy the generated Forwarded Address, such as
https://<id>-7071.uks1.devtunnels.ms. Right-click port7071, select Port Visibility β Public, and confirm the warning. -
Point the SharePoint trigger to the local Function host.
On macOS or Linux:
sh ./infra/scripts/configure-trigger.sh \ --target local \ --callback-base-url "https://<id>-7071.uks1.devtunnels.ms"
On Windows:
pwsh ./infra/scripts/configure-trigger.ps1 ` -Target Local ` -CallbackBaseUrl "https://<id>-7071.uks1.devtunnels.ms"
-
Upload a newly named copy of
sample-data/contoso-rfp.pdfto SharePoint. The trigger polls every five minutes and sends the callback through the public dev tunnel. Rerun the configuration command whenever the forwarded address changes.
To return the trigger to the deployed Function App, run azd deploy. The
postdeploy hook recreates the trigger with the Azure callback URL.
- RFP arrives. A file is uploaded to the monitored SharePoint document library.
- Trigger. The Connector Namespace polls the SharePoint "When a file
is created" trigger (
GetOnNewFileItems) and calls the function'sOnNewFilecallback. The trigger returns properties only, so the content must be fetched separately. - Fetch content. The function calls the SharePoint "Get file content"
action (
SharePointOnlineClient.GetFileContentAsync) with the file identifier from the trigger payload and decodes the connector's JSON Base64 binary response into the original document bytes. - Extract the document. The original file bytes are sent to Content
Understanding's
prebuilt-layoutanalyzer, which returns OCR text and document structure without invoking a generative model. - Route the RFP. The function parses the customer and numbered headings in
the
Required Capabilitiessection, then maps recognized capability terms to SME roles with deterministic routing rules. - Notify. The function builds an Adaptive Card and posts it to a Teams
channel with the "Post card in a chat or channel" action
(
TeamsClient.PostCardToConversationAsync).
If you previously provisioned an Azure OpenAI or standalone Document Intelligence version of this sample into the same azd environment, incremental ARM deployment retains those unused Cognitive Services accounts. After the Content Understanding workflow is deployed and verified, list the legacy accounts:
az cognitiveservices account list --resource-group "$(azd env get-value resourceGroupName)" --query "[?kind=='OpenAI' || kind=='FormRecognizer'].{name:name,kind:kind}" -o tableIf the command returns an account, confirm that no other application uses it, then delete the unused resource:
az cognitiveservices account delete --resource-group "$(azd env get-value resourceGroupName)" --name <legacy-openai-account-name>To delete the complete sample environment instead:
azd down --purge- Function-app user-assigned managed identity: Calls the SharePoint and
Teams connection runtime URLs and Azure Content Understanding. It has an
access policy on each connection and the
Cognitive Services Userrole on the Microsoft Foundry resource. - Application Insights: The Function worker uses the same managed identity to export OpenTelemetry. Local authentication is disabled on the Application Insights and Microsoft Foundry resources.
- Connector Namespace system managed identity: Polls the SharePoint trigger and delivers callbacks.
- Callback authorization: Uses the
connector_extensionsystem key on the callback URL. This sample does not use App Service built-in authentication.
The scripts in infra/scripts complete setup that cannot be handled entirely by
the Bicep deployment:
authorize-connections.ps1/.sh: Opens the OAuth consent flow and waits for the SharePoint and Teams connections to become authenticated. Connections that are already authenticated are skipped. This script is needed because Bicep creates the connections, but a user must grant consent.createlocalsettings.ps1/.sh: Generates the ignoredlocal.settings.jsonfile from the current azd deployment outputs. It preserves an existing file unless explicitly forced.postprovision.ps1/.sh: Runs connector authorization and local settings generation.azd provisioninvokes this platform-specific script through thepostprovisionhook.configure-trigger.ps1/.sh: Creates the SharePoint new-file trigger and points it to a local dev tunnel or the deployed Function App. It adds theconnector_extensionsystem key to the callback URL. This script runs after the function is available because the callback URL and key are not known earlier. Changing targets requires the trigger to be deleted and recreated.postdeploy.ps1/.sh: Configures the trigger for Azure and checks both connection authorizations.azd deployruns the platform-specific script through thepostdeployhook, replacing any local callback with the deployed Function App callback.
If the summary card is not posted to the specified Teams channel, check the following:
- In the Connector Namespace portal, verify that the SharePoint trigger's
callback URL points to the intended local dev tunnel or deployed Function App
and includes the correct
connector_extensionaccess key. - For local testing, verify that port
7071is forwarded and the dev tunnel visibility is Public. - Ensure Azurite is running before starting the function locally and configuring the trigger.
- Allow up to five minutes for the SharePoint trigger to detect a newly created file. Each new test upload produces another Teams post.
- Leave
AZURE_CLIENT_IDempty inlocal.settings.jsonfor local development. The Function App's managed identity client ID is only needed when running in Azure. - Confirm the Teams connection was authorized by a user who can access the destination team and channel. Private channels are supported when that user is a channel member.
- Confirm the uploaded file is in a format supported by Content Understanding.
- Confirm the RFP contains a numbered
Required Capabilitiessection. The deterministic parser intentionally returns empty capability and SME lists rather than inventing requirements that are not present.
- Azure Functions Connector extension β the trigger binding used here.
- Azure Connectors .NET SDK β typed clients for SharePoint, Teams, and other connectors.
- Azure Content Understanding
prebuilt-layoutanalyzer β non-generative OCR and layout extraction used by this sample. - Content Understanding region support β regions available for the Microsoft Foundry resource.