agentleFS
Microsoft AzureKnown publisherAzure/azure-functions-connector-extension/.github/skills/webhook-trigger-registration/SKILL.md

webhook-trigger-registration

Skill1 starsChanged 37 days agoMITView on GitHub

Register Connector Namespace trigger configs that use Webhook delivery to Azure Functions. USE WHEN: setting up callback delivery for connector triggers, configuring notificationDetails, QueryString callback authentication, Function App destination metadata, local dev tunnels, or troubleshooting Webhook trigger configs. NOT FOR: Functions host-pull Poll delivery (use poll-trigger-registration), connection setup (use connection-setup), or extension internals.

  • Installs packages

The file

We split this skill into the rules it gives an agent

12 rules. About 0 are common, 6 are distinctive, and 6 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈6

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 6

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Add the connector extension and SDK packages:

    line 92
  2. Very specific:

    Add to requirements.txt (include packages based on your approach):

    line 108
  3. Very specific:

    Add to local.settings.json for Python 3.10-3.12:

    line 123
  4. Distinctive:

    Delete any sample HTTP trigger functions and replace with:

    line 146
  5. Very specific:

    Before starting the Function App, ensure Azurite (local Azure Storage emulator) is running.

    line 223
  6. Very specific:

    Start the Function App with --enableAuth to secure the endpoint behind a system key:

    line 276
  7. Distinctive:

    If you haven't logged in with GitHub before, you'll be prompted to sign in

    line 293
  8. Distinctive:

    Use the tunnel URL from the Local Development with Port Forwarding section above.

    line 338
  9. Distinctive:

    If no logs appear:

    line 465
  10. Very specific:

    Verify the trigger config state = Enabled (Step 3)

    line 466
  11. Very specific:

    Check that the callback URL is correct and publicly accessible (for local dev tunnels, verify --allow-anonymous or per-port anonymous …

    line 467
  12. Distinctive:

    When done testing, always revoke public access:

    line 516

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Connector Webhook Trigger Registration for Azure Functions

Registers polling trigger configs on a Connector Namespace so that connector events (new email, new file, etc.) call back to your Azure Function via the ConnectorTrigger extension.

This workflow configures Connector Namespace polling with Webhook delivery to the Function callback endpoint. For the extension's DeliveryMode = Poll host-pull contract, use the poll-trigger-registration skill.

When to Use

  • Developer needs a connector trigger (e.g., "when a new email arrives in Office365")
  • Developer has an existing Connector Namespace connection (use the connection-setup skill first if not)
  • Developer needs to scaffold a new Function App project with [ConnectorTrigger]
  • Developer needs to wire the callback URL from a deployed or local Function App

Prerequisites

  • Azure CLI ≥ 2.75.0 installed and authenticated (az login)
  • connector-namespace CLI extension installed (see connection-setup skill)
  • Connector Namespace with a connected connector (see connection-setup skill)
  • The Connector Namespace must have a system-assigned managed identity enabled

Key Concepts

Extension Webhook Endpoint

The connector extension (Microsoft.Azure.Functions.Worker.Extensions.Connector) registers a webhook route on the Function App:

POST /runtime/webhooks/connector?functionName={FunctionName}
  • functionName must exactly match the [Function("...")] attribute name
  • connector_extension is a system key auto-generated when the extension loads
  • Store the key separately in notificationDetails.authentication as a QueryString value named code; do not embed it in callbackUrl
  • Locally (func start), the system key is not enforced

Trigger Config vs Connection

Connector Namespace
├── connections/
│   └── office365-conn         ← auth + runtime URL (connection-setup skill)
└── triggerConfigs/
    └── onnewemail-trigger     ← poll + callback config (THIS skill)

Scaffolding a New Function App Project

1. Initialize with azd

.NET

azd init -t functions-quickstart-dotnet-azd

Python

azd init -t functions-quickstart-python-azd

TypeScript

azd init -t functions-quickstart-typescript-azd

JavaScript

azd init -t functions-quickstart-javascript-azd

Note: The azd init templates create a host.json file. For non-.NET languages (Node.js, Python, etc.), update host.json to use the preview extension bundle version 4.42.0 or greater:

{
    "version": "2.0",
    "extensionBundle": {
        "id": "Microsoft.Azure.Functions.ExtensionBundle.Preview",
        "version": "[4.42.0, 5.0.0)"
    }
}

2. Install connector packages

Add the connector extension and SDK packages:

.NET

Install the latest pre-release NuGet packages:

# Connector trigger binding for the isolated worker (recommended)
dotnet add package Microsoft.Azure.Functions.Worker.Extensions.Connector --prerelease

# Connector SDK (typed payloads and action clients)
dotnet add package Azure.Connectors.Sdk --prerelease

Python

Add to requirements.txt (include packages based on your approach):

# Python 3.13+
azure-functions>=2.4.0b1; python_version>='3.13'
# Python 3.10-3.12 (Webhook only)
azure-functions>=1.26.0b3; python_version<'3.13'

# Typed Functions integration for Office 365 OnNewEmail
azurefunctions-extensions-connectors

# Optional typed models for other connector operations; not required for str payloads
azure-connectors

Add to local.settings.json for Python 3.10-3.12:

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "python",
    "PYTHON_ISOLATE_WORKER_DEPENDENCIES": "1"
  }
}

TypeScript / JavaScript

npm install @azure/functions@4.15.1-preview
npm install @azure/functions-extensions-connectors@0.0.1-preview
npm install @azure/connectors

3. Replace the HTTP trigger with a ConnectorTrigger function

Delete any sample HTTP trigger functions and replace with:

.NET

using Microsoft.Azure.Functions.Worker.Extensions.Connector;
using Azure.Connectors.Sdk.Office365.Models;

[Function("OnNewEmail")]
public void OnNewEmail(
    [ConnectorTrigger]
    Office365OnNewEmailTriggerPayload payload)
{
    foreach (GraphClientReceiveMessage email in payload.Body.Value)
    {
        _logger.LogInformation(
            "From: {From}, Subject: {Subject}",
            email.From,
            email.Subject);
    }
}

Python

import azure.functions as func
import azurefunctions.extensions.connectors.office365 as office365
import logging
from typing import List

app = func.FunctionApp()

@app.function_name(name="OnNewEmail")
@app.connector_trigger(arg_name="emails")
def on_new_email(emails: List[office365.ClientReceiveMessage]) -> None:
    logging.info("OnNewEmail trigger received")

    for email in emails:
        logging.info(f"Subject: {email.subject}")
        logging.info(f"From: {email.from_}")

TypeScript

import { app, InvocationContext } from "@azure/functions";

app.generic("OnNewEmail", {
  trigger: { type: "connectorTrigger", name: "payload" },
  handler: async (payload: unknown, context: InvocationContext) => {
    const data = typeof payload === "string" ? JSON.parse(payload) : payload;
    for (const email of data?.body?.value ?? []) {
      context.log(`From: ${email.from}, Subject: ${email.subject}`);
    }
  },
});

OneDrive for Business example (OnNewFile trigger):

import { app, InvocationContext } from "@azure/functions";

app.generic("OnNewFile", {
  trigger: { type: "connectorTrigger", name: "payload" },
  handler: async (payload: unknown, context: InvocationContext) => {
    const data = typeof payload === "string" ? JSON.parse(payload) : payload;
    for (const file of data?.body?.value ?? []) {
      context.log(`File: ${file.name}, Path: ${file.path}`);
    }
  },
});

4. Run locally

Before starting the Function App, ensure Azurite (local Azure Storage emulator) is running. The Functions runtime requires AzureWebJobsStorage for local development.

Start Azurite (required for local development)

Choose one option:

Option 1: VS Code Extension (simplest)

  1. Install the Azurite extension from VS Code Marketplace
  2. Open Command Palette (Ctrl+Shift+P) and run "Azurite: Start"

Option 2: npm global install

npm install -g azurite
azurite

Option 3: npx (no installation)

npx azurite

Verify local.settings.json includes:

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "<language>"
  }
}

Start the Function App

func start

The extension logs the webhook endpoint at startup:

Connector endpoint: http://localhost:7071/runtime/webhooks/connector

⚠️ Troubleshooting: If func start fails with AzureWebJobsStorage or storage connection error:

  • Verify Azurite is running in a separate terminal
  • Confirm local.settings.json has "AzureWebJobsStorage": "UseDevelopmentStorage=true"
  • Check that no other process is using port 10000 (Azurite's default port)
  • Clear identity cache if switching between cloud/local storage: %LOCALAPPDATA%\.IdentityService\cache (Windows) or ~/.IdentityService/cache (Mac/Linux)

Local Development with Port Forwarding

To test triggers locally, you need to expose your local Function App so the Connector Namespace can reach it.

Start the Function App with --enableAuth to secure the endpoint behind a system key:

func start --enableAuth

⚠️ Important: Always use --enableAuth when exposing your app via a dev tunnel. Without it, your function endpoint is completely unauthenticated on the public internet.

Confirm the app is running on http://localhost:7071.

🔔 Confirm: Is the Function App running with --enableAuth? (Yes / No)

⚠️ Security Warning: The following steps expose your local Function App to the public internet. Only proceed for local testing and if you understand the implications.

VS Code port forwarding

  1. Navigate to the Ports view in the Panel region (Ports: Focus on Ports View) and select Forward a Port
  2. If you haven't logged in with GitHub before, you'll be prompted to sign in
  3. Enter port 7071 — port forwarding starts and the Ports view updates to show the forwarded port and its Forwarded Address (e.g., https://<id>-7071.uks1.devtunnels.ms)
  4. Change the visibility by right-clicking on the port and selecting Port Visibility → Public. Public ports don't require sign in

🔔 Confirm: Is the port forwarded with Public visibility and do you see the tunnel URL in the Ports panel? (Yes / No)

Create a dev tunnel with CLI (alternative to VS Code)

💡 Prefer the CLI over VS Code UI? Use the devtunnel CLI instead — see Dev Tunnels CLI quickstart.

devtunnel user login  # choose the option that matches your org policy
devtunnel create <your-tunnel-id> -a  # custom tunnel id for a persistent, reusable URL
devtunnel port create <your-tunnel-id> -p 7071
devtunnel host <your-tunnel-id> --allow-anonymous

🍎 Mac note: If devtunnel host returns Tunnel service error: Request not permitted. Unauthorized tunnel creation access: Anonymous does not have 'create' access scope, your login didn't actually stick. On macOS, devtunnel user login without a provider flag silently leaves you as Anonymous. Fix:

devtunnel user login -g     # GitHub (or -m Microsoft, -e Entra)
devtunnel user show         # confirm — should NOT print "Anonymous"
devtunnel host -p 7071 --allow-anonymous

If your corp tenant blocks --allow-anonymous on the default tunnel domain, drop the flag and grant per-port anon access instead:

devtunnel host -p 7071
devtunnel access create -p 7071 --anonymous   # in a separate terminal

Registering a Trigger Config

Step 1: Get the Callback URL

Deployed Function App

$resourceGroup = "<resource-group>"
$functionAppName = "<function-app-name>"
$functionName = "<function-name>"  # must match [Function("...")] attribute

$connectorKey = az functionapp keys list -g $resourceGroup -n $functionAppName --query "systemKeys.connector_extension" -o tsv
$callbackUrl = "https://$functionAppName.azurewebsites.net/runtime/webhooks/connector?functionName=$functionName"

Local development (with dev tunnel)

Use the tunnel URL from the Local Development with Port Forwarding section above.

Important: Always start your Function App with --enableAuth when using a dev tunnel. Without it, your function endpoint is completely unauthenticated on the public internet.

func start --enableAuth

Retrieve the connector_extension system key from local Azurite storage:

# Use the well-known Azurite connection string
# See: https://learn.microsoft.com/azure/storage/common/storage-use-emulator#authorize-with-shared-key-credentials
$connStr = "<azurite-connection-string>"

# Find the most recent host.json blob
$blobs = az storage blob list --container-name azure-webjobs-secrets --connection-string $connStr -o json | ConvertFrom-Json
$blobName = ($blobs | Sort-Object { $_.properties.lastModified } | Select-Object -Last 1).name

# Download and parse
az storage blob download --container-name azure-webjobs-secrets --name $blobName --connection-string $connStr --file host-keys.json --no-progress
$keys = Get-Content host-keys.json | ConvertFrom-Json
$connectorKey = ($keys.systemKeys | Where-Object { $_.name -eq "connector_extension" }).value

Build the callback URL:

$tunnelUrl = "<your-tunnel-url>"  # from VS Code Ports panel, e.g., https://<id>-7071.uks1.devtunnels.ms
$functionName = "<function-name>"
$callbackUrl = "$tunnelUrl/runtime/webhooks/connector?functionName=$functionName"

Note: The tunnel must have Public visibility (anonymous access). The Connector Namespace cannot authenticate to private tunnels. We use connector extension keys for auth instead of the tunnel's built-in auth.

Step 2: Create Trigger Config

The trigger create command needs --notification-details passed via a temp file because the callback URL contains query-string characters:

$resourceGroup = "<resource-group>"
$namespaceName = "<namespace-name>"
$subscriptionId = "<subscription-id>"
$functionAppName = "<function-app-name>"

$triggerName = "<trigger-config-name>"   # e.g., "onnewemail-trigger"
$connectionName = "<connection-name>"    # e.g., "office365-conn"
$connectorName = "<connector-name>"      # e.g., "office365"
$operationName = "<operation-name>"      # e.g., "OnNewEmailV3" or "OnNewFilesV2"
$functionName = "<function-name>"        # must match [Function("...")] attribute

# Keep the callback URL free of secrets. Connector Namespace adds the `code`
# query parameter from the separate authentication object when invoking it.
$notificationDetails = @{
    callbackUrl = $callbackUrl
    authentication = @{
        type  = "QueryString"
        name  = "code"
        value = $connectorKey
    }
} | ConvertTo-Json -Depth 3 -Compress

# Write notification details to a temp file to avoid shell quoting issues.
$notifFile = Join-Path $env:TEMP "notification-details.json"
$notificationDetails | Set-Content -Path $notifFile -NoNewline

# Delete any existing trigger with the same name
az connector-namespace trigger delete `
    -g $resourceGroup --namespace $namespaceName `
    -n $triggerName --yes 2>$null | Out-Null

az connector-namespace trigger create `
    -g $resourceGroup --namespace $namespaceName `
    -n $triggerName `
    --connection-details "{connectionName:$connectionName,connectorName:$connectorName}" `
    --operation-name $operationName `
    --notification-details "@$notifFile" `
    --description "$connectorName $operationName -> $functionName" `
    --metadata "{destinationType:functionApp,functionAppName:$functionAppName,functionAppResourceGroup:$resourceGroup,functionAppSubscriptionId:$subscriptionId,functionName:$functionName,recurrenceFrequency:Minute,recurrenceInterval:'5'}" `
    -o none

Remove-Item $notifFile -ErrorAction SilentlyContinue

The --metadata object identifies the destination to the Connector Namespace portal so it can render the backend as a Function App. Keep the Function App subscription, resource group, app name, and function name aligned with the callback destination. Runtime delivery and authentication are controlled by notificationDetails; the metadata object does not replace the callback URL or its QueryString authentication.

Trigger parameters — add --parameters for connector-specific inputs:

# Office 365 OnNewEmailV3 — specify folder
--parameters "[{name:folderPath,value:'Inbox'}]"

# OneDrive for Business OnNewFilesV2 — specify folder
--parameters "[{name:folderId,value:'root'}]"

# SharePoint OnNewItems — specify site and list
--parameters "[{name:dataset,value:'https://contoso.sharepoint.com/sites/MySite'},{name:table,value:'MyList'}]"

Note: Trigger parameters are connector-specific. Use az connector-namespace connection invoke with the connector's operations endpoint to discover available triggers and their required parameters.

Step 3: Verify Trigger Config

az connector-namespace trigger show `
    -g $resourceGroup --namespace $namespaceName `
    -n $triggerName `
    --query "properties.{operation:operationName,state:state,callback:notificationDetails.callbackUrl,authenticationType:notificationDetails.authentication.type,authenticationName:notificationDetails.authentication.name}" `
    -o table

Expected: state = Enabled, authenticationType = QueryString, and authenticationName = code. The authentication value is intentionally not displayed.

Step 4: Test the Trigger

Trigger the connector event (e.g., send an email to the Office 365 inbox, upload a file to OneDrive root folder, etc.). Watch the Function App logs for execution:

Expected success output:

Executing 'Functions.OnNewEmail' (Reason='', Id=9c4e2415-bc91-430c-bda4-d8953725a432)
Received Microsoft 365 OnNewEmail trigger
Email received from: user@contoso.com
Email subject: Test Email
Executed 'Functions.OnNewEmail' (Succeeded, Id=9c4e2415-bc91-430c-bda4-d8953725a432, Duration=123ms)

If no logs appear:

  1. Verify the trigger config state = Enabled (Step 3)
  2. Check that the callback URL is correct and publicly accessible (for local dev tunnels, verify --allow-anonymous or per-port anonymous access)
  3. Review Troubleshooting section below
  4. Query trigger run history:
    az connector-namespace trigger run list `
        -g $resourceGroup --namespace $namespaceName `
        --trigger-name $triggerName -o table

Step 5: Update Callback URL

To point an existing trigger config to a different callback (e.g., after redeploying or switching tunnels):

$newCallbackUrl = "https://<your-function-host>/runtime/webhooks/connector?functionName=<FunctionName>"
$connectorKey = az functionapp keys list `
    -g $resourceGroup -n $functionAppName `
    --query "systemKeys.connector_extension" -o tsv

$notificationDetails = @{
    callbackUrl = $newCallbackUrl
    authentication = @{
        type  = "QueryString"
        name  = "code"
        value = $connectorKey
    }
} | ConvertTo-Json -Depth 3 -Compress

$notifFile = Join-Path $env:TEMP "notification-details.json"
$notificationDetails | Set-Content -Path $notifFile -NoNewline

az connector-namespace trigger update `
    -g $resourceGroup --namespace $namespaceName `
    -n $triggerName `
    --notification-details "@$notifFile"

Remove-Item $notifFile -ErrorAction SilentlyContinue

Step 6: List All Trigger Configs

az connector-namespace trigger list `
    -g $resourceGroup --namespace $namespaceName `
    -o table

Step 7: Clean up after testing

When done testing, always revoke public access:

  1. In the Ports panel, right-click the forwarded port
  2. Select Stop Forwarding Port (or set visibility back to Private)

⚠️ Do not leave your local Function App publicly exposed longer than necessary.

Troubleshooting

Common Errors

ErrorCauseFix
Could not find member 'connectionName'Used connectionName at top levelUse --connection-details shorthand: connectionName=… connectorName=…
Could not find member 'callbackUrl'Put callbackUrl at the trigger properties levelPass a JSON file to --notification-details containing both callbackUrl and authentication
Trigger provisions but never firesMissing/incorrect callback or authentication detailsVerify callbackUrl, authentication.type = QueryString, and authentication.name = code; rotate and reapply the system key if needed
unrecognized arguments: --namespaceAzure CLI < 2.75.0Run az upgrade or use --connector-namespace-name

Polling Interval

The Connector Namespace polls the connector every 1-5 minutes. After polling detects new content, it POSTs the payload to your callback URL.

Reference

For a complete mapping of trigger operations to function signatures across .NET, Python, and TypeScript (including which typed payload to use and how to specify the function), see Operations to Functions Signature Match.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.