azure functions
400 TopicsMeet the Hosted Skills Canvas: Build, Run, and Debug in GitHub Copilot
Build, run, and debug event-driven AI apps right inside GitHub Copilot. The new Azure Functions Hosted Skills canvas brings instructions, triggers, and live results into one workspace—so you can spend less time switching tools and more time building.
641Views2likes0CommentsBring Azure SRE Agent into your developer workflow with the new canvas
You’re on call. A customer-reported incident lands, Azure SRE Agent picks it up from your incident management system, investigates, and gives you a starting diagnosis. If you’re like me, you want to check that diagnosis yourself, and you want to do it where you already work: in GitHub Copilot, with your code, your tools, and your own access to the data. The new Azure SRE Agent canvas puts the investigation right next to your Copilot chat. A canvas is an interactive panel beside the conversation, with context and actions you and Copilot can both use. You get SRE Agent’s findings plus everything SRE doesn’t have: your code, your private data sources, and what you know about the app. In the GitHub Copilot App, sessions are on the left, Copilot chat is in the middle, and the SRE canvas is on the right. You can use the same canvas in Copilot CLI sessions that support canvases. I now have SRE's findings alongside my own context in Copilot chat. I can keep digging here and decide what to send back to SRE, and when. Let me show you the inner loop with an incident from September 22. It’s a Contoso demo, but it’s modeled on a real one we hit in production. Gateway health looked available while orders and payments failed. I'll use SRE's diagnosis as a starting point, check the historical telemetry with my own authorized access, and work out what the evidence does and doesn't establish. I’ll share findings with the team, and anything worth keeping goes back to SRE Agent. Start with the incident With the plugin enabled, I prompt Copilot to open Azure SRE Agent and choose my saved demo1-dt-snow favorite. I can also discover agents in a subscription I choose or connect through a caller-authorized external URL. For this investigation, I open INC0922001, "Trading order and payment requests failing while gateway health remains available," and choose Connect this thread. Selecting a row lets me browse; connecting makes it the investigation for this conversation. The September incident as recorded in SRE Agent. Then I ask: Summarize this connected September 22 investigation. Separate the reported diagnosis from observed evidence. Give the incident time window, and list what still needs independent verification. The SRE record reports a stopped PostgreSQL database. It says the gateway passed its health check, but Orders and payments requests failed after about 15 seconds. That's useful context, but a passing health check doesn't mean orders and payments are working. I want to check the logs for myself. SRE’s diagnosis and suggested recovery. Suggested isn’t the same as confirmed, which is where I start digging. Get the historical window right The first query assumption I check is the date. This incident needs September 22, 17:30 to 19:45 UTC, not the last 24 hours. I use the Log Analytics workspace behind the demo and ask: Reconstruct the failure check for this historical window. Show the complete query and target before running it. Then break down the request routes and status codes. The reconstructed query and its saved result give me something concrete to inspect. This is my own reconstruction, not a replay of SRE’s query, so the counts differ a little from SRE’s snapshot. The query runbook includes the complete KQL, working Log Analytics API route, and scope for review. The result contains 120 gateway Orders 500 records, 115 gateway payments 502 records, corresponding backend failures, and 119 database connection timeouts. These are telemetry records, not distinct customer requests: the gateway and backend can record the same call. Exporter authentication errors and later route 404s are separate signatures, not more examples of the same database failure. Inspect the requests, dependencies, and exceptions Grouped counts tell me where to look. To understand the failure, I want to look at some of the actual log entries from that time window, not just the totals: Show the Orders requests, downstream dependencies, and database timeouts for this window. Where operation IDs are populated, follow the same operation. Tell me what is correlated and what is only nearby in time. The saved exception signature says the connection to the database endpoint timed out. That supports a connectivity problem; it doesn't independently prove the database was stopped. To connect a particular exception to a particular request, I need matching trace identifiers, not just nearby timestamps. The raw-record and correlation queries are the next checks, with their own execution approval. Widen my check beyond Orders An Orders-focused query is a useful starting point, not an impact assessment. I ask: Keep the same source and time window, but remove the Orders operation and dependency-target restrictions. What else has 5xx failures or the same database-connectivity signature? Keep unrelated errors separate. The saved broader result already includes payments, which SRE also identified. My goal is to verify that scope independently and see whether the wider query adds anything. I won't turn two affected paths into an all-services outage. This data also has blank application-role names, so I use the recorded operations and dependency targets rather than invent an application-name filter. Test the competing explanation A gateway deployment or connection setting could also cause trouble. I want to compare that explanation with the database diagnosis: Preview the historical deployment and configuration records needed for this window, plus independent database state or availability evidence. Do not use today's settings or health as proof of September 22. If the records aren't available, leave the check open. The first SRE record reports the database as stopped, but its Activity Log check found no stop event and may not have covered the right scope. Independent incident-window records would help distinguish stopped state from configuration or network problems. Current settings can't establish historical state, and missing or expired change history can't rule out a deployment. For now, the logs support connectivity trouble. Historical database state and the competing configuration explanation still need checking. The first record also doesn't confirm completed recovery; a later recurrence doesn’t tell us what happened on September 22.. Bring in evidence SRE may not have, or go where it cannot The logs got me to a connectivity problem. The next question is capacity: was something starved? That evidence lives in a Kusto cluster SRE Agent can’t reach, but I can. Private Connectors let me use my own authorized access from inside the same investigation. Copilot-private connections are separate from the SRE Agent connectors. Teams is ready for the update. For a performance or capacity question, I can choose an authorized connection, review the source and complete query, and approve its time range and filters before execution. Then I can compare the relevant measurements and units. This is an extension of the investigation. Granting capacity is a separate action with its own approval. I can do it from here, but it’s my call, not the agent’s. The connection stays private to Copilot. Connecting an SRE investigation doesn't give the agent my datasource permissions. If I want SRE to use a finding, I separately preview and approve the evidence to share. For checks I run locally, I use my identity and network access; a cloud-backed connector uses its provider's network path. Review the updated team message The team update should carry the refined root-cause analysis, including its limits: SRE's reported diagnosis, the independently observed failures, the scope, and the checks still open. Next prompt: Draft a September 22 update to send to the team in Teams. Include the reported diagnosis, observed Orders and payments failures, connectivity evidence, competing explanations, and remaining checks. Include the investigation link. Show me the exact message and destination before sending. That gives the team a useful starting point for the next check, with a link back to the investigation. Continue in the terminal I can carry the same investigation into a canvas-capable Copilot CLI session. I connect INC0922001 explicitly in that conversation; the App's connection doesn't carry over. Connect to INC0922001 from September 22. Give me a TL;DR that separates the reported diagnosis, independently checked evidence, and remaining questions. The terminal is another place where I like to work, and I still want to follow the same evidence and use my own context and tools. The SRE canvas provides tools to Copilot CLI too. The IDE and terminal guide covers a separate connection through the Azure MCP Server. So who stopped the database? We did, for the demo. The real production incident that inspired this was resource exhaustion, which is less satisfying, because every engineer wants it to be the database. It’s better when you can prove it. Try the canvas for your next investigation The useful loop isn't just reading an incident summary from an agent. It's taking SRE's starting diagnosis into Copilot, inspecting the evidence, testing alternatives, and bringing in authorized data from my own tools. Sometimes that confirms SRE Agent got it right. Sometimes it doesn’t. Either way, what I learn goes back into the agent’s memory so the next incident starts smarter. In this one, I learned we’re not resilient enough to this kind of failure yet, and we have hardening work to do. Install from Customize > Plugins > Awesome Copilot > Azure SRE Agent, start a new session, and ask: Open Azure SRE Agent canvas. Watch the demo video, bring an existing investigation, and tell us what you think. The canvas guide has setup details. Also see the October SRE Agent update and explore the other Azure canvases.360Views1like2CommentsFrom Form to Action: A Governed, Event-Driven AI Intake Pipeline with Azure Integration Services
A production-minded reference architecture using Azure Integration Services and Microsoft Foundry to turn document-heavy intake into governed, traceable work. TL;DR: Calling a model is one step in AI-powered intake. Production readiness depends on everything around the model: authenticated entry, controlled model consumption, durable asynchronous processing, explicit failure handling, and one correlation ID carried from the gateway to the final event. This post walks through a permit-intake reference architecture on Azure Integration Services and a companion repository you can run offline, without an Azure subscription. The Problem: Intake is where AI Prototypes Stall Many teams have built an AI prototype that reads a scanned form, extracts the key fields, and returns clean, structured data. That prototype proves the model can do the job. Taking it to production raises a different set of questions: Who is allowed to call this, and how many times per minute? What happens when the model returns a 429 or the CRM is down for maintenance? Which team consumed those tokens? Can you show me everything that happened to application #00417? Who is notified when a record is created, and what breaks when we add another subscriber? None of those questions is about the model, yet each one can decide whether the solution ships. The underlying customer problem is broader than “How do we read a form?” It’s “How do we accept responsibility for a submission, process it reliably across slow or unavailable dependencies, and give applicants and operators a trustworthy status?” That framing moves the design from a single AI call to a governed system of connected services. In this post, I use a fictional county’s building-permit intake as the running example. A resident submits a permit packet, and the county needs to validate it, extract the key fields, score it against intake policy, record it in a case system, and notify the right people, with a complete audit trail. The same shape appears in insurance first notice of loss, benefits enrollment, supplier onboarding, and claims intake. The repository keeps the domain in schemas and configuration: replace the schemas and labels, and keep the boundaries. My thesis is simple: the hard part of production AI intake is connecting the services around the model reliably and under governance, not calling the model. The model is one component of the system. It isn’t the system. Scope note: This is a production-minded reference architecture, not a production-ready template. The people, permit data, identifiers, and URLs in this post are synthetic, and the screenshots and trace rows show only that synthetic data. The repository favors demonstration clarity: public endpoints, simple SKUs, and simulated fallbacks. Architecture at a Glance Figure 1. The governed, event-driven permit-intake flow. One correlation ID follows the permit from the gateway to the published event. The design separates the acceptance path from the processing path. API Management owns the caller-facing contract and hands the work to a queue. Slow or failure-prone work happens after that hand-off. The flow reads left to right: Portal → API Management → Logic App → Service Bus → Function → Case system → Event Grid → Subscribers │ Document Intelligence + Foundry model (through the AI gateway) Responsibility Service Why this service Governed front door and AI gateway Azure API Management One policy boundary for business APIs and model APIs Low-code validation, enrichment, and routing Azure Logic Apps Business-visible run history; easy to change routing rules Durable hand-off and back-pressure Azure Service Bus Retries, duplicate detection, and dead-lettering Processing worker Azure Functions Scales with queue depth; runs the shared orchestrator Field extraction Azure Document Intelligence Layout and key-value extraction from the packet Policy-oriented scoring Azure OpenAI in Microsoft Foundry Models Rubric-based completeness and consistency review Fan-out to downstream consumers Azure Event Grid Decoupled subscribers via CloudEvents One correlation ID across the flow Application Insights End-to-end transactions plus one KQL join Discovery and reuse Azure API Center A catalog of the Permits and model APIs The case-system box is deliberately not a CRM product. The repository’s crm.py is an adapter: an in-memory stub by default, or an HTTP call to whatever CRM_BASE points at. Swap in Dynamics 365 or your records system without changing the orchestration contract. Let’s walk through the design decisions that matter most. API Management is more than a Proxy API Management is often introduced as a proxy. In this architecture, it’s the governance boundary, and it governs two surfaces. Figure 2. One gateway governs both the business API and the model API. Surface 1: The Permits API The Entra-protected variant of the inbound policy (permits-api.policy.xml, abridged) authenticates, throttles, and stamps a correlation ID before any backend code runs: <inbound> <base /> <validate-azure-ad-token tenant-id="{{tenant-id}}" failed-validation-httpcode="401"> <audiences><audience>api://ais-demo-permits</audience></audiences> </validate-azure-ad-token> <rate-limit-by-key calls="60" renewal-period="60" counter-key="@(context.Subscription?.Key ?? context.Request.IpAddress)" /> <set-variable name="correlationId" value="@(context.Request.Headers.GetValueOrDefault("X-Correlation-Id", Guid.NewGuid().ToString()))" /> <set-header name="X-Correlation-Id" exists-action="override"> <value>@((string)context.Variables["correlationId"])</value> </set-header> <set-backend-service backend-id="permit-intake-logicapp" /> </inbound> Three lines of intent are worth calling out: validate-azure-ad-token rejects missing, invalid, or expired Microsoft Entra tokens with a 401 at the edge, so unauthenticated requests are rejected before they reach the backend. It knows the Entra issuers and signing keys for your tenant, so there’s no OpenID configuration URL to maintain. rate-limit-by-key gives each consumer its own budget and returns 429 with Retry-After when it’s exceeded. The correlation ID is created here (or accepted from a trusted caller) and echoed back on the 202 Accepted response. That single value is how you’ll find this permit later. The deployed demo authenticates callers with API Management subscription keys and applies the same rate limit and correlation ID. To adopt the Entra variant, register an app for the api://ais-demo-permits audience, register a backend for the Logic App trigger, and then swap the policy. Surface 2: The Model API (The AI Gateway) The Function doesn’t call the model endpoint directly. It calls the Azure OpenAI v1 API through API Management, where a second policy applies (abridged): <inbound> <base /> <llm-content-safety backend-id="content-safety-backend" shield-prompt="true"> <categories output-type="EightSeverityLevels"> <category name="Hate" threshold="4" /> <category name="SelfHarm" threshold="4" /> <category name="Sexual" threshold="4" /> <category name="Violence" threshold="4" /> </categories> </llm-content-safety> <llm-token-limit counter-key="@(context.Subscription?.Id ?? "anonymous")" tokens-per-minute="20000" token-quota="2000000" token-quota-period="Monthly" estimate-prompt-tokens="false" /> <llm-emit-token-metric namespace="ais-demo-ai-gateway"> <dimension name="Subscription" value="@(context.Subscription?.Name ?? "unknown")" /> <dimension name="Department" value="@(context.Request.Headers.GetValueOrDefault("x-dept", "unknown"))" /> </llm-emit-token-metric> <set-header name="api-key" exists-action="delete" /> <authentication-managed-identity resource="https://cognitiveservices.azure.com" /> <set-backend-service backend-id="foundry-models-backend" /> </inbound> In order, that policy: llm-content-safety sends the prompt to Azure AI Content Safety, with Prompt Shields for injection attempts, and returns 403 for prompts that exceed the configured thresholds, before they reach the model. llm-token-limit gives each consumer a tokens-per-minute budget (429) and a monthly token quota (403). llm-emit-token-metric publishes token usage to Application Insights with a department dimension for showback or chargeback. Managed identity authenticates the gateway to the model. The caller’s API Management subscription key is stripped, and local (key) auth is disabled on the Foundry resource, so the Function doesn’t hold a model key, which helps reduce the risk of key leakage. The backend also has a circuit breaker that trips after three 429 or 5xx responses within a minute, pauses calls for a minute (or for the Retry-After period the model returns), and then resumes. The repository includes sample KQL for token monitoring and chargeback views. The key point: conventional APIs and AI endpoints share one governed boundary. Your security team reviews one set of policies, and your platform team gets one place to see who is calling what. Discovery: Azure API Center API Management governs runtime traffic; Azure API Center is where people find the APIs. The repository links API Center to API Management, so the Permits APIs and the model API are cataloged with their definitions, lifecycle stage, and environment. Before a team builds another intake API or model proxy, they search the catalog and still call API Management at runtime. API Center isn’t on the request path. Why the Flow is Asynchronous The Permits API returns 202 Accepted with a correlation ID. It doesn’t wait for extraction, scoring, or the case system. That’s deliberate: document analysis, model calls, record writes, and notifications all have different latency and failure behavior, and holding the caller’s request open couples the caller to each of them. Governance boundary: 202 means the platform durably accepted responsibility for the permit. It doesn’t mean extraction succeeded, a model approved anything, or a record was committed. The repository enforces that literally. On the direct route (POST /permits), API Management writes to Service Bus and checks the answer before it says yes: <send-request mode="new" response-variable-name="sbresponse" timeout="20" ignore-error="true"> <!-- POST to https://{namespace}.servicebus.windows.net/permits-in/messages with managed identity --> </send-request> <choose> <when condition="@(context.Variables.GetValueOrDefault<IResponse>("sbresponse") == null || context.Variables.GetValueOrDefault<IResponse>("sbresponse").StatusCode != 201)"> <return-response> <set-status code="503" reason="Service Unavailable" /> <!-- plus Retry-After --> </return-response> </when> </choose> <!-- only now: return 202 Accepted with X-Correlation-Id --> If Service Bus doesn’t confirm the write (an error status, a timeout, or no connection), the caller gets 503 with Retry-After, not a 202 for work that wasn’t queued. Logic Apps: Validate, Enrich, Route The Logic App workflow checks the payload, attaches the correlation ID, and hands the message to Service Bus with its own managed identity. If required fields are missing, it returns 400 immediately. Here’s the send step: "Send_to_Service_Bus": { "type": "Http", "inputs": { "method": "POST", "uri": "https://@{parameters('serviceBusNamespace')}.servicebus.windows.net/@{parameters('permitsQueue')}/messages", "headers": { "BrokerProperties": "@{string(outputs('Broker_properties'))}" }, "body": "@outputs('Enrich_message')", "authentication": { "type": "ManagedServiceIdentity", "audience": "https://servicebus.azure.net" } } } A preceding Broker_properties step sets MessageId to the parcel number and CorrelationId to the request’s correlation ID. Bicep loads this same workflow JSON when it deploys the Logic App, so each deployment applies the workflow definition from the repository. Business analysts can read the run history in the Azure portal. If the county adds a new permit type or a routing rule, that change happens in the workflow, not in Python. Design choice: The template deploys both routes. POST /permits-orchestrated goes through this Logic App. POST /permits uses a direct-enqueue policy that writes straight to Service Bus with the API Management managed identity; it’s the portal’s default and the route the trace below uses. Use the direct route when you don’t need low-code enrichment. Keep the Logic App when business users own the routing rules. Service Bus: The Durable Hand-off The permits-in queue is configured in Bicep with: maxDeliveryCount: 5, a message is delivered up to five times (the first attempt plus four retries) before it’s moved to the dead-letter subqueue. requiresDuplicateDetection: true with a 10-minute window, resubmitting the same messageId (the parcel number) doesn’t create a second message. See duplicate detection. deadLetteringOnMessageExpiration: true, expired messages are kept for inspection instead of being dropped. Functions: One Orchestrator, Many Hosts A Service Bus–triggered Function (Python 3.14 on Flex Consumption) reads the correlation ID from the message properties and runs a framework-agnostic orchestrator. A message without a permit type raises an error, so Service Bus retries it and then moves it to the dead-letter queue. The orchestrator does four things in order: extract → score → record → publish. def process_permit(permit: dict, correlation_id: str) -> ProcessResult: extracted = document_intelligence.extract_fields(permit) # Document Intelligence compliance = ai_gateway.score_compliance(extracted) # model via the API Management AI gateway status = "IntakeReview" if compliance.score >= settings.compliance_threshold else "NeedsAttention" permit_id = crm.create_permit_record(extracted, compliance, correlation_id, status) event_grid.publish_permit_created(PermitEvent(permitId=permit_id, complianceScore=compliance.score, correlationId=correlation_id)) ... Notice what the model doesn’t do: it doesn’t approve or deny anything. The score (0–100, against a rubric for completeness, consistency, and policy fit) only decides which human review state the permit lands in, IntakeReview or NeedsAttention, and that state is written to the case record. The call uses the Responses API on gpt-5.4-mini with structured output: the response must match a JSON schema (score, missing, flags), so downstream code receives a consistent, schema-validated shape. There’s no dated api-version to maintain: client = OpenAI( base_url=f"{apim_base}/openai/v1/", api_key=subscription_key, default_headers={"api-key": subscription_key, "x-dept": "permitting"}, ) review = client.responses.parse( model="gpt-5.4-mini", instructions=RUBRIC, input=fields_json, text_format=Review, reasoning={"effort": "low"}, store=False, ).output_parsed The caller authenticates to API Management, not to the model: the api-key header carries the caller’s API Management subscription key (the template renames this API’s subscription-key header to api-key). The OpenAI client requires api_key and also sends it as a bearer token, but the gateway replaces that header with its own managed-identity token before calling the model. For production, swap the subscription key for a Microsoft Entra token (api_key=token_provider) and validate it at the gateway. The same process_permit function is hosted by the Function, by a FastAPI service for the portal, and by the Python SDK walkthrough. That keeps the demo, the tests, and the deployed worker running the same code. Event Grid: Fan-out without Coupling After the CRM write, the Function publishes a PermitCreated CloudEvent. Two subscribers react independently: an analytics/audit queue and, when you supply a webhook URL, a resident notification webhook. Each subscription makes up to 30 delivery attempts over 24 hours. Adding a third subscriber, such as a GIS update or an inspector scheduling system, is a new subscription. It’s not a change to the permit flow. Commands and Events are Different Things Service Bus carries a command: “process this permit.” Event Grid carries a fact: “this permit was created,” published only after the record write succeeds. Keeping them separate makes retries, ownership, and subscriber behavior easier to reason about, a poison message can be retried and dead-lettered without a PermitCreated event going out for work that didn’t finish. Four rules follow: Message identity: a stable message ID (the parcel number) and the correlation ID as a message property. In the repository. Idempotency: a redelivered message must confirm earlier work, not create a duplicate record. Production work. Dead-letter ownership: someone watches the queue, reads the reason codes, and is authorized to replay. Operational. Status: callers need a status resource (accepted, processing, needs review, completed, failed) that doesn’t expose document contents. Production works the repository returns only the correlation ID. How Failures are Handled and What Still Needs Work A production-minded design makes failure behavior explicit before the happy path is trusted. The table separates what the repository does today from what still belongs on the production path: Failure What happens in the repository Still needed for production Caller has no valid token 401 at API Management (Entra policy variant); no backend call Entra validation on every public API Caller exceeds its rate limit 429 with Retry-After Per-consumer products and quotas Payload missing required fields Orchestrated route: 400 from the Logic App, passed back by API Management. Direct route: accepted, then dead-lettered by the Function if type is missing The same validation on the direct route Queue unavailable Direct route: 503 with Retry-After. Orchestrated route: the Logic App run fails and returns an error. Neither returns 202 An alert on enqueue failures Malformed message on the queue Delivered up to five times, then dead-lettered (MaxDeliveryCountExceeded) A named owner and a replay procedure Prompt blocked by content safety, or monthly token quota spent 403 from the gateway; Service Bus redelivers the message, then dead-letters it Route to NeedsAttention with a reason instead of dead-lettering Model unavailable (429 or 5xx from the gateway or model) The OpenAI client retries twice, then Service Bus redelivers the message and finally dead-letters it. The backend circuit breaker pauses calls to the model while it’s tripped Route to NeedsAttention with the reason model_unavailable, keeping the deterministic validation results Case system unavailable The Function fails before publishing; the message is redelivered An idempotency key on the record write Event Grid unavailable after the record write The Function fails; a retry can repeat the record write A transactional outbox for the event A notification subscriber is down Event Grid retries that subscriber only (30 attempts over 24 hours) — Two rows deserve a closer look. Model unavailable: today, a model failure retries and then dead-letters the permit. A better design keeps the deterministic validation results, routes the permit to NeedsAttention with a reason such as model_unavailable, and never treats a missing score as approval. Case system unavailable: the record write is the commit point for PermitCreated, so the event must never go out before it. Idempotency keys plus an outbox let a committed record and its event be reconciled. A prototype doesn’t need these safeguards; a production service does. AI is Optional and Bounded The non-AI capabilities like durable acceptance, extraction, deterministic validation, the case record, and events are useful with generative AI turned off. The model’s score is advisory: it prioritizes review and surfaces gaps. It must never quietly become the approval decision. Extraction output is evidence to validate, not ground truth. Deterministic rules own required fields, allowed values, and hard policy constraints. In the repository, the Logic App rejects a packet without name or type, and the Function dead-letters a message without type. The model doesn’t enforce either rule. The model’s output is schema-constrained (score, missing, flags) and labeled advisory. Low scores, conflicting evidence, and model failures route to people. Logs carry identifiers and stage outcomes, not document bodies. Before any real workload, evaluate the scorer in Microsoft Foundry with synthetic or properly governed labeled data. The scorer is a direct model call through API Management, not a Foundry agent, so use dataset-based evaluators and graders, and keep an authorized person in the loop for consequential decisions. Trace One Permit End to End Governance is only useful if you can prove what happened. Here’s one synthetic permit created through the demo environment. Submit. The client posts the permit to API Management: { "name": "[Applicant Name]", "type": "Building", "parcel": "AIS-TT-102242", "applicantEmail": "[Applicant Email]" } Govern and accept durably. On the direct /permits route, API Management applies its policies, stamps a correlation ID, writes the message to permits-in, and only after Service Bus answers 201, returns 202 Accepted with X-Correlation-Id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx. Process. The Function picks up the message and extracts the fields. This run had no document attached, so it used the repository’s synthetic extraction fallback; attach a document URL to exercise Document Intelligence. Score, behind content safety. The Function calls the model through the AI gateway. Content Safety and Prompt Shields screen the prompt first; then gpt-5.4-mini returns a score of 22 and lists missing applicant contact details and a project description. Status: NeedsAttention, which is the expected result for a packet missing those details. (Simulated mode scores the same packet 100.) Record and publish. The case-system adapter creates P-E6980B0C, and the Function publishes PermitCreated; Event Grid delivers it to the analytics queue. Reconstruct. One query returns the whole journey. The portal shows the same journey to a business audience: Figure 3. The portal’s processing result and hop-by-hop trace. This screenshot is from simulated mode, so all values are synthetic and the applicant is shown as a placeholder. In Application Insights the journey is two operations. The intake operation is the API Management request and its Service Bus write. The processing operation is one end-to-end transaction: the Function, its call back through the AI gateway, the Content Safety checks, the model call, and the Event Grid publish. The Service Bus hop doesn’t carry W3C trace context, so the correlation ID is the join key, API Management logs it as a header, and the Function’s log lines name it. This query finds the operations that mention it: let cid = '<correlation-id>'; let hits = union AppRequests, AppDependencies, AppTraces, AppExceptions | where OperationId == cid or Message has cid or tostring(Properties) has cid; let ops = hits | where isnotempty(OperationId) and OperationId != '00000000000000000000000000000000' | distinct OperationId; union (union AppRequests, AppDependencies, AppExceptions | where OperationId in (ops)), (hits | where Type == 'AppTraces') | project TimeGenerated, Type, Name = coalesce(Name, Message, OuterMessage), ResultCode, DurationMs, AppRoleName | order by TimeGenerated asc For the permit above, it returned: Operation Hop Component Result Duration (ms) Intake POST /permits API Management 202 1,471 Intake POST /permits-in/messages Service Bus enqueue 201 177 Processing ServiceBusProcessor.ProcessMessage Azure Functions — 5,875 Processing POST /openai/v1/responses AI gateway (called by the Function) 200 4,542 Processing POST /contentsafety/text:analyze Content Safety 200 777 Processing POST /contentsafety/text:shieldPrompt Prompt Shields 200 235 Processing POST /openai/v1/responses Foundry model (gpt-5.4-mini) 200 2,705 Processing POST /api/events Event Grid publish 200 83 Durations come from one run in a demo environment, including cold starts. They aren’t performance figures. Figure 4. The processing operation for the same permit in Application Insights. The durations match the table above. Resource names and tenant identifiers are redacted. When a caseworker asks, “What happened to application #00417?”, the answer can come from one query instead of separate log searches across teams. Run it Locally, then Deploy it The repository is designed so you can understand the architecture before you spend anything on Azure. 1. Offline Simulation (no Azure Subscription) uv sync --extra dev # Python 3.14 cp .env.example .env # SIMULATED_MODE=true by default uv run ais-demo # runs steps B1–B8 end to end In simulated mode, each adapter (Service Bus, Document Intelligence, the model, CRM, Event Grid, Azure Monitor) is replaced by an in-memory stand-in with the same interface. The walkthrough submits a permit, enqueues it, extracts and scores it, writes the record, publishes the event, dead-letters a deliberately malformed message, and prints the correlated trace. The test suite (uv run pytest) covers the same paths. To see the business view, start the API and portal together: ./scripts/setup.sh ./scripts/app.sh start # API on :8000, portal on :5173 (Node.js 24 LTS) 2. Same Interface, Azure-backed Adapters Each adapter module checks one setting: if settings.simulated_mode or not settings.servicebus_fqdn: return _consume_simulated(handler) return _consume_live(handler, settings) # azure-servicebus + DefaultAzureCredential Set SIMULATED_MODE=false, provide the endpoints, and the same orchestrator talks to real services. Live adapters authenticate with DefaultAzureCredential, so the deployed Function uses its managed identity for Service Bus, Document Intelligence, Event Grid, and Azure Monitor. Both Python hosts export OpenTelemetry to Application Insights, and the Function’s log lines name the correlation ID. 3. Deploy with Bicep and Clean up az group create -n rg-ais-demo -l eastus2 az deployment group what-if -g rg-ais-demo \ --template-file infra/main.bicep --parameters infra/main.bicepparam az deployment group create -g rg-ais-demo \ --template-file infra/main.bicep --parameters infra/main.bicepparam # or: azd provision --preview && azd provision One deployment creates and wires the whole stack: Service Bus and Event Grid (with the analytics subscription, plus the notification webhook subscription when you pass notificationWebhookUrl), Document Intelligence, a Microsoft Foundry resource with the gpt-5.4-mini deployment, the Logic App workflow, a Flex Consumption Function App on Python 3.14, API Management with all three APIs (/permits, /permits-orchestrated, and /openai) and their policies, API Center, and the managed-identity role assignments it needs. Local (key) auth is disabled on Service Bus, Event Grid, Document Intelligence, and the Foundry resource. The template defaults to the BasicV2 tier of API Management; the sample main.bicepparam sets the classic Developer tier to match the existing demo environment, so change apimSku for a new environment. For a faster inner loop, pass deployApim=false. After provisioning, publish the Function code; the deployment guide has the details, and a GitHub Actions workflow runs the same what-if and provision steps with OpenID Connect. When you’re done: az group delete -n rg-ais-demo --yes --no-wait The repository also includes a demo track that tells the same story two ways: an Azure portal click-through for business stakeholders (steps A1–A15) and a Python SDK walkthrough for engineers (steps B1–B8). Production Boundaries: What this Reference doesn’t Do This repository is a production-minded reference architecture: it anticipates production concerns while staying candid about what it doesn’t prove. A production path starts from your organization’s requirements and platform standards. Here’s the gap: Area In the repository Before production Networking Public endpoints Private endpoints, virtual network integration, and a WAF in front of internal API Management (reference architecture) Identity Managed identity to Service Bus, Event Grid, Document Intelligence, and the Foundry resource, with local auth disabled; API Management subscription keys for callers (an Entra token policy is provided) Microsoft Entra token validation on every public API; the Function’s API Management subscription key in Key Vault or replaced by Entra auth AI safety Content Safety and Prompt Shields at the gateway; structured output; advisory-only scoring Tuned thresholds and blocklists, response-side checks, and offline and continuous evaluation gated in CI Data retention In-memory or simulated CRM A real system of record with retention, residency, and deletion policies agreed with records owners Consistency Retries, duplicate detection, and dead-lettering Idempotency keys and an outbox for the record write and event publish Caller status The correlation ID A status resource (accepted, processing, needs review, completed, failed) Access control One demo subscription Per-consumer products, least-privilege RBAC, and separated duties for operators and reviewers Operational ownership Demo scripts Dashboards, alerts on dead-letter depth and token budgets, runbooks, and named owners for every queue Human review Score routes to a review state Documented reviewer workflow, override capture, and periodic sampling of AI-assisted decisions Figure 5. The same services in a network-isolated API Management landing zone. The repository’s production path maps each of these items to the Azure Well-Architected Framework pillars. It also sketches how the AI gateway grows into a multi-region front door with backend pools, circuit breakers, and semantic caching, without changing the governance policies you already have. Map the Article to the Repository Article Repository location Shared pipeline src/ais_demo/orchestrator.py Governed intake (direct, orchestrated, Entra variant) apim/policies/permits-api*.xml AI gateway apim/policies/aoai-api.policy.xml Logic App workflow integration/logicapp/permit-intake-workflow.json Durable command and dead-lettering src/ais_demo/integrations/service_bus.py Extraction src/ais_demo/integrations/document_intelligence.py Advisory scoring src/ais_demo/integrations/ai_gateway.py Case-system adapter src/ais_demo/integrations/crm.py Events src/ais_demo/integrations/event_grid.py Trace join src/ais_demo/integrations/monitor.py Infrastructure infra/main.bicep and infra/modules/ Runbooks docs/deployment-guide.md and docs/demo-track.md Production path docs/production-path.md Key Takeaways Put the model behind the same gateway as your APIs. Content safety, token limits, usage metrics, and managed identity help turn model access into something you can govern and attribute. Accept fast, process durably and mean it. Return 202 only after the queue confirms the message. That helps shield residents from downstream outages and gives operators a dead-letter queue to inspect instead of a lost request. Keep AI advisory. Let the model route work to people. Don’t let it make the decision. Create one correlation ID at the edge and carry it through each hop. It’s one of the simplest audit trails you can build. Decouple what happens next. Event Grid lets new consumers subscribe without touching the intake flow. What to do Next Agree on the acceptance contract: exactly when 202 is returned and how callers check status. Run the simulated portal and Python walkthroughs with one synthetic correlation ID. Test the failure paths: queue unavailable, poison message, model unavailable, case system unavailable, and a trace gap. Add idempotent record writes and an outbox for event publication. Evaluate the advisory model separately from deterministic rules and human approval. Complete an architecture, privacy, security, and Well-Architected review for your subscription before using real data. Try It Repository: github.com/honestypugh2/ais-demo. Start with uv run ais-demo, no Azure subscription required. Adapt it: Change the schemas in src/ais_demo/schemas/permit.py, update USE_CASE_PROFILE and the samples in data/, and keep the topology. Go deeper: AI gateway capabilities in API Management · Service Bus dead-letter queues · Event-driven architecture style · Investigate transactions in Application Insights Related reading: Grounding Copilot Studio Agents with Azure AI Search and Foundry IQ Models will change. The work around them is what moves intake from form to action: a governed front door, durable hand-offs, advisory scoring, and one correlation ID from submission to the final event. Clone the repository, run it offline, and adapt the schemas to your own intake process. Comments and questions are welcome below. The sample code in this post and the companion repository is provided "as is" under the MIT license for demonstration purposes. It uses synthetic data only and isn't intended for production use without additional security, reliability, and compliance review. Nothing in this post is legal, compliance, security, or deployment assurance, and it doesn't represent a customer implementation. The county and applicant in this example are fictional. Check product availability, quotas, model access, and regional capabilities for your own subscription.317Views0likes0CommentsBring Azure Logic Apps connectors into your .NET applications with the Azure Connectors SDK
Modern business applications rarely work in isolation. An order exception might need an email, a support case might need a Teams notification, and a completed document might need to be stored in SharePoint. Developers can build each integration directly against a service API, but that also means owning different authentication models, request and response shapes, pagination rules, and error handling for every service. The Azure Connectors SDK brings the typed experience of a .NET client library to Connector Namespace, which makes the Logic Apps and Power Platform connector runtime available to developers as a programmable integration layer. Your application can call connector operations through generated clients and models while Azure manages the connection to the external service. In this post, we will build a small Azure function app that reacts to a new email through a Connector trigger and sends a formatted review email through the Office 365 Outlook connector. The same pattern can then be extended to many other business systems. Preview: Azure Connectors SDK is currently in preview. Preview features are provided without a service-level agreement and are not recommended for production workloads. APIs and setup steps can change before general availability. Why this matters Integration code often starts with one HTTP call and grows into a collection of service-specific concerns. The Azure Connectors SDK gives .NET developers: Typed clients, request models, and response models generated from connector contracts Async operations with cancellation-token support Azure credential integration for authenticating the calling application Consistent connector exceptions and diagnostics Built-in retry support for transient failures Pagination, binary payload, and dynamic-schema support where connectors expose them OpenTelemetry integration for tracing connector calls Access to a broad connector ecosystem that includes Microsoft 365, Azure services, data platforms, storage, messaging, and SaaS applications The SDK does not put external-service credentials in your application. Instead, your code authenticates to a Connector Namespace connection. That connection holds the authorization for Office 365 Outlook or another target service. Scenario: notify operations about an order exception Imagine an order service that flags transactions for manual review by sending an email to an Office 365 mailbox. The subject identifies the order and the body briefly explains the exception: To: orders@contoso.com Subject: Order exception: SO-10482 Body: Customer requested a delivery-address change after payment. Connector Namespace polls the mailbox for new messages matching the subject filter and calls an Azure function app's Connector trigger. The function reads the typed email payload and sends a formatted review email to a separate operations inbox. Unlike an HTTP-triggered function, there is no application endpoint or 202 Accepted response to invoke. Prerequisites You need: An Azure subscription in which Connector Namespace is available Permission to create Connector Namespace resources and connections .NET 10 SDK Azure Functions Core Tools v4 Azure CLI Azure Developer CLI ( azd ) for deployment, or an existing .NET 10 isolated-worker function app An Office 365 account that can authorize the connection and send email At the time of writing, Connector Namespace is available in a limited set of preview regions. Confirm current region availability before creating the resource. 1. Create an Office 365 connection First sign in and select the subscription that will own the Connector Namespace: az login az account set --subscription "<subscription-id>" $subscriptionId = "<subscription-id>" $resourceGroup = "<resource-group>" $namespaceName = "<connector-namespace-name>" $connectionName = "office365-orders" $location = "<supported-azure-region>" $apiVersion = "2026-05-01-preview" $nsId = "/subscriptions/$subscriptionId/resourceGroups/$resourceGroup/providers/Microsoft.Web/connectorGateways/$namespaceName" Create a Connector Namespace with a system-assigned managed identity: $namespace = @{ location = $location identity = @{ type = "SystemAssigned" } properties = @{} } | ConvertTo-Json -Depth 5 -Compress $tempFile = Join-Path $env:TEMP "connector-namespace.json" [System.IO.File]::WriteAllText($tempFile, $namespace) az rest --method PUT ` --uri "https://management.azure.com${nsId}?api-version=$apiVersion" ` --body "@$tempFile" ` --headers "Content-Type=application/json" ` --output none Remove-Item $tempFile Create the Office 365 connection: $connection = @{ properties = @{ connectorName = "office365" } } | ConvertTo-Json -Depth 5 -Compress $tempFile = Join-Path $env:TEMP "connector-connection.json" [System.IO.File]::WriteAllText($tempFile, $connection) az rest --method PUT ` --uri "https://management.azure.com${nsId}/connections/${connectionName}?api-version=$apiVersion" ` --body "@$tempFile" ` --headers "Content-Type=application/json" ` --output none Remove-Item $tempFile The new connection is not authorized yet. Request an OAuth consent link and open it in your browser: $consentRequest = @{ parameters = @( @{ redirectUrl = "https://portal.azure.com" parameterName = "token" } ) } | ConvertTo-Json -Depth 5 -Compress $tempFile = Join-Path $env:TEMP "connector-consent.json" [System.IO.File]::WriteAllText($tempFile, $consentRequest) $consent = az rest --method POST ` --uri "https://management.azure.com${nsId}/connections/${connectionName}/listConsentLinks?api-version=$apiVersion" ` --body "@$tempFile" ` --headers "Content-Type=application/json" ` --output json | ConvertFrom-Json Remove-Item $tempFile Start-Process $consent.value[0].link Complete the sign-in and consent flow. Then verify that the connection reports Connected : az rest --method GET ` --uri "https://management.azure.com${nsId}/connections/${connectionName}?api-version=$apiVersion" ` --output json | ConvertFrom-Json | Select-Object -ExpandProperty properties | Select-Object -ExpandProperty statuses 2. Allow your local identity to call the connection OAuth consent authorizes the connection to call Office 365. A separate access policy authorizes your application to call the connection. This distinction is important: without the policy, the runtime call fails with HTTP 403 and a missing connection ACL message. Add an access policy for the identity currently signed in to Azure CLI: $userObjectId = az ad signed-in-user show --query id --output tsv $tenantId = az account show --query tenantId --output tsv $policy = @{ properties = @{ principal = @{ type = "ActiveDirectory" identity = @{ objectId = $userObjectId tenantId = $tenantId } } } } | ConvertTo-Json -Depth 6 -Compress $tempFile = Join-Path $env:TEMP "connector-policy.json" [System.IO.File]::WriteAllText($tempFile, $policy) az rest --method PUT ` --uri "https://management.azure.com${nsId}/connections/${connectionName}/accessPolicies/local-dev?api-version=$apiVersion" ` --body "@$tempFile" ` --headers "Content-Type=application/json" ` --output none Remove-Item $tempFile Access-policy changes can take a few minutes to reach the connector runtime. Finally, retrieve the connection runtime URL. Treat this value as application configuration rather than source code: $connection = az rest --method GET ` --uri "https://management.azure.com${nsId}/connections/${connectionName}?api-version=$apiVersion" ` --output json | ConvertFrom-Json $connectionRuntimeUrl = $connection.properties.connectionRuntimeUrl 3. Create the Azure function app Create an isolated-worker function app with the current Core Tools template. Some Core Tools v4 versions do not offer net10.0 as a func init --target-framework option; use the .NET 9 template, then change the generated project's TargetFramework to net10.0 before building: func init ConnectorsSdkOrderAlerts --worker-runtime dotnet-isolated --target-framework net9.0 Set-Location ConnectorsSdkOrderAlerts In ConnectorsSdkOrderAlerts.csproj , set: <TargetFramework>net10.0</TargetFramework> Add the connector packages: dotnet add package Azure.Connectors.Sdk --version 0.13.0-preview.1 dotnet add package Azure.Identity dotnet add package Microsoft.Azure.Functions.Worker.Extensions.Connector --version 0.2.0-alpha Register an Azure credential and only the connector client this function needs. DefaultAzureCredential can use your Azure CLI identity locally and the function app's managed identity in Azure: using Azure.Connectors.Sdk; using Azure.Core; using Azure.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; var host = new HostBuilder() .ConfigureFunctionsWorkerDefaults() .ConfigureServices((hostContext, services) => { services.AddSingleton<TokenCredential>(new DefaultAzureCredential()); services.AddOffice365Client( hostContext.Configuration.GetSection("Connectors:Office365")); }) .Build(); host.Run(); Add the function. The Connector trigger binds the OnNewEmailV3 callback to a typed payload. Filter the subject again in code, HTML-encode untrusted email fields, and send the review message through the generated SendEmailInput model: using System.Text.Encodings.Web; using Azure.Connectors.Sdk.Office365; using Azure.Connectors.Sdk.Office365.Models; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Extensions.Connector; public sealed class OperationalAlertFunctions { private const string SubjectPrefix = "Order exception:"; private readonly Office365Client _office365Client; public OperationalAlertFunctions(Office365Client office365Client) { this._office365Client = office365Client; } [Function("OnOrderExceptionEmail")] public async Task OnOrderExceptionEmailAsync( [ConnectorTrigger] Office365OnNewEmailTriggerPayload payload, CancellationToken cancellationToken) { foreach (var message in payload.Body?.Value ?? []) { if (message?.Subject?.StartsWith( OperationalAlertFunctions.SubjectPrefix, StringComparison.OrdinalIgnoreCase) != true) { continue; } var orderId = message.Subject[OperationalAlertFunctions.SubjectPrefix.Length..].Trim(); if (string.IsNullOrWhiteSpace(orderId)) { continue; } var email = new SendEmailInput { To = "operations@contoso.com", Subject = $"Order {orderId} requires review", Body = "<h2>Order review required</h2>" + $"<p><strong>Order:</strong> {HtmlEncoder.Default.Encode(orderId)}</p>" + $"<p><strong>From:</strong> {HtmlEncoder.Default.Encode(message.From ?? string.Empty)}</p>" + $"<p><strong>Reason:</strong> {HtmlEncoder.Default.Encode(message.BodyPreview ?? string.Empty)}</p>" }; await this._office365Client.SendEmailAsync(email, cancellationToken); } } } Use a monitored mailbox or folder separate from the operations inbox. Keep the outgoing subject different from the trigger's subject filter so the notification cannot trigger itself. For production workloads, account for repeated callbacks and partial failures within a batch before performing non-idempotent actions such as sending mail. 4. Deploy and register the Connector trigger Keep the runtime URL out of source control. Set it for local development, then deploy the function app with a system-assigned managed identity and the same setting in its application configuration: $env:Connectors__Office365__ConnectionRuntimeUrl = $connectionRuntimeUrl The public Office 365 connector-trigger sample is a downloadable .NET 10 function app with deployment infrastructure, an OnNewEmail binding, and post-deploy trigger registration. Use its azd up workflow or deploy this function app through your existing pipeline. A locally running Functions host is not reachable by the Connector Namespace polling callback unless you provide a secure public callback endpoint. Once the function app is deployed, add its managed identity to the connection's access policies, just as you added your CLI identity for local development. For a system-assigned identity, get its principal ID and use it in the access policy from step 2 instead of $userObjectId : $functionAppName = "<function-app-name>" $functionObjectId = az functionapp identity show ` --resource-group $resourceGroup --name $functionAppName ` --query principalId --output tsv $policy = @{ properties = @{ principal = @{ type = "ActiveDirectory" identity = @{ objectId = $functionObjectId tenantId = $tenantId } } } } | ConvertTo-Json -Depth 6 -Compress $tempFile = [System.IO.Path]::GetTempFileName() try { [System.IO.File]::WriteAllText($tempFile, $policy) az rest --method PUT ` --uri "https://management.azure.com${nsId}/connections/${connectionName}/accessPolicies/order-alert-function?api-version=$apiVersion" ` --body "@$tempFile" ` --headers "Content-Type=application/json" ` --output none } finally { Remove-Item $tempFile } Obtain the Connector extension's system key and the function app's actual default hostname, then construct the callback URL for the deployed function. Do not assume that the hostname is <function-app-name>.azurewebsites.net ; the default hostname format can vary: $connectorExtensionKey = az functionapp keys list ` --resource-group $resourceGroup --name $functionAppName ` --query "systemKeys.connector_extension" --output tsv $functionAppHostname = az functionapp show ` --resource-group $resourceGroup --name $functionAppName ` --query "defaultHostName" --output tsv $callbackUrl = "https://$functionAppHostname/runtime/webhooks/connector?functionName=OnOrderExceptionEmail&code=$connectorExtensionKey" The system key is a secret. Do not log, publish, or commit the callback URL. Create the Office 365 polling trigger config using the same connection and a subject filter matching the incoming order messages: $triggerConfig = @{ properties = @{ operationName = "OnNewEmailV3" connectionDetails = @{ connectorName = "office365" connectionName = $connectionName } notificationDetails = @{ callbackUrl = $callbackUrl httpMethod = "Post" } parameters = @( @{ name = "folderPath"; value = "Inbox" } @{ name = "subjectFilter"; value = "Order exception:" } ) metadata = @{ destinationType = "functionApp" functionAppName = $functionAppName functionAppResourceGroup = $resourceGroup functionAppSubscriptionId = $subscriptionId functionName = "OnOrderExceptionEmail" recurrenceFrequency = "Minute" recurrenceInterval = "5" } } } | ConvertTo-Json -Depth 6 -Compress $tempFile = [System.IO.Path]::GetTempFileName() try { [System.IO.File]::WriteAllText($tempFile, $triggerConfig) az rest --method PUT ` --uri "https://management.azure.com${nsId}/triggerConfigs/order-exception-email?api-version=$apiVersion" ` --body "@$tempFile" ` --headers "Content-Type=application/json" ` --output none } finally { Remove-Item $tempFile } Confirm that the trigger config reports Enabled . Send a test email to the monitored mailbox, and allow for the polling interval before checking the function logs and the operations inbox: az rest --method GET ` --uri "https://management.azure.com${nsId}/triggerConfigs/order-exception-email?api-version=$apiVersion" ` --query "properties.{operation:operationName,state:state}" ` --output table Confirm all three outcomes before considering the test complete: The trigger config is Enabled and the function logs show the invocation. The operations inbox receives a review email with the expected order ID and reason. Unrelated messages do not produce review emails. From local development to Azure For a deployed function app, use its managed identity instead of a developer identity: Enable a system-assigned or user-assigned managed identity on the function app. Add that identity as an access policy on the Connector Namespace connection. Register DefaultAzureCredential or ManagedIdentityCredential as the TokenCredential . Store the connection runtime URL in function app configuration. Protect the Connector trigger callback with the Connector extension system key; use a stronger authorization model for production as appropriate. The external-service credential remains in the connector connection. The function app presents its Azure identity when it calls the runtime URL. The Connector Namespace uses the callback URL and its system key to invoke the function. Go beyond email The order-alert example is intentionally small, but the programming model supports much broader integration scenarios: Microsoft 365: work with Outlook, SharePoint, Teams, OneDrive, Excel, Planner, Forms, and Microsoft Entra ID-backed operations. Azure services: connect to Blob Storage, queues, tables, Event Grid, Event Hubs, Service Bus, Key Vault, Azure Monitor Logs, and Azure Data Factory. Business applications: integrate with Salesforce, Dynamics 365, Jira, ServiceNow-style workflows, DocuSign, Zendesk, and other SaaS systems represented in the connector catalog. Event-driven applications: register polling triggers that call back to an Azure function app when connector events occur. Data-rich APIs: consume paginated operations, binary content, and operations whose input or output schema is resolved dynamically. Observable integrations: capture connector activity in distributed traces with OpenTelemetry. Because connector clients are typed, developers can discover operations through IntelliSense and keep connector calls alongside the rest of their application code. The connection layer handles authorization to the external system, while Azure identity and access policies govern which application may use that connection. Troubleshooting The call returns 403 with “missing connection ACL.” The external OAuth connection may be healthy, but the calling Azure identity is not authorized. Add an access policy for the Azure CLI user during local development or the workload managed identity after deployment. Allow time for the policy to propagate. The connection remains in an error state. Complete the OAuth consent flow and query the connection again. Make sure the account used for consent has access to the target service. The client fails during startup because the runtime URL is missing. Make sure Connectors__Office365__ConnectionRuntimeUrl is present in the process environment. In a larger application, register only the connector clients whose configuration is available. Authentication works locally but fails in Azure. The local Azure CLI user and the function app managed identity are different principals. Add a separate connection access policy for the function app identity and use a managed-identity-capable credential in the deployed host. The email trigger does not fire. Verify the trigger config is Enabled , its operation is OnNewEmailV3 , its folderPath and subjectFilter match the test email, and its callback uses the deployed function's OnOrderExceptionEmail name and Connector extension system key. Polling is not instantaneous. Get started Explore the Azure Connectors SDK repository, try the samples, and tell us which connectors and application patterns would be most useful for your team. Azure Connectors SDK for .NET Azure Connectors SDK samples Downloadable Office 365 connector-trigger function app Azure Logic Apps connectors overview Azure Identity client library for .NET473Views0likes0CommentsBulletproof agents with the durable task extension for Microsoft Agent Framework
Today, we're thrilled to announce the public preview of the durable task extension for Microsoft Agent Framework. This extension transforms how you build production-ready, resilient and scalable AI agents by bringing the proven durable execution (survives crashes and restarts) and distributed execution (runs across multiple instances) capabilities of Azure Durable Functions directly into the Microsoft Agent Framework. Now you can deploy stateful, resilient AI agents to Azure that automatically handle session management, failure recovery, and scaling, freeing you to focus entirely on your agent logic. Whether you're building customer service agents that maintain context across multi-day conversations, content pipelines with human-in-the-loop approval workflows, or fully automated multi-agent systems coordinating specialized AI models, the durable task extension gives you production-grade reliability, scalability and coordination with serverless simplicity. Key features of the durable task extension include: Serverless Hosting: Deploy agents on Azure Functions with auto-scaling from thousands of instances to zero, while retaining full control in a serverless architecture. Automatic Session Management: Agents maintain persistent sessions with full conversation context that survives process crashes, restarts, and distributed execution across instances Deterministic Multi-Agent Orchestrations: Coordinate specialized durable agents with predictable, repeatable, code-driven execution patterns Human-in-the-Loop with Serverless Cost Savings: Pause for human input without consuming compute resources or incurring costs Built-in Observability with Durable Task Scheduler: Deep visibility into agent operations and orchestrations through the Durable Task Scheduler UI dashboard Click here to create and run a durable agent # Python endpoint = os.getenv("AZURE_OPENAI_ENDPOINT") deployment_name = os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME", "gpt-4o-mini") # Create an AI agent following the standard Microsoft Agent Framework pattern agent = AzureOpenAIChatClient( endpoint=endpoint, deployment_name=deployment_name, credential=AzureCliCredential() ).create_agent( instructions="""You are a professional content writer who creates engaging, well-structured documents for any given topic. When given a topic, you will: 1. Research the topic using the web search tool 2. Generate an outline for the document 3. Write a compelling document with proper formatting 4. Include relevant examples and citations""", name="DocumentPublisher", tools=[ AIFunctionFactory.Create(search_web), AIFunctionFactory.Create(generate_outline) ] ) # Configure the function app to host the agent with durable session management app = AgentFunctionApp(agents=[agent]) app.run() // C# var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT"); var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT") ?? "gpt-4o-mini"; // Create an AI agent following the standard Microsoft Agent Framework pattern AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential()) .GetChatClient(deploymentName) .CreateAIAgent( instructions: """You are a professional content writer who creates engaging, well-structured documents for any given topic. When given a topic, you will: 1. Research the topic using the web search tool 2. Generate an outline for the document 3. Write a compelling document with proper formatting 4. Include relevant examples and citations""", name: "DocumentPublisher", tools: [ AIFunctionFactory.Create(SearchWeb), AIFunctionFactory.Create(GenerateOutline) ]); // Configure the function app to host the agent with durable thread management // This automatically creates HTTP endpoints and manages state persistence using IHost app = FunctionsApplication .CreateBuilder(args) .ConfigureFunctionsWebApplication() .ConfigureDurableAgents(options => options.AddAIAgent(agent) ) .Build(); app.Run(); Why the durable task extension? As AI agents evolve from simple chatbots to sophisticated systems handling complex, long-running tasks, new challenges emerge: Conversations span multiple days and weeks, requiring persistent state across process restarts, crashes, and disruptions. Tool calls might take longer than typical timeouts allow, needing automatic checkpointing and recovery. High-volume workloads require elastic scaling across distributed instances to handle thousands of concurrent agent conversations. Multiple specialized agents need coordination with predictable, repeatable execution for reliable business processes. Agents sometimes must wait for human approval before proceeding, ideally without consuming resources. The Durable Extension addresses these challenges by extending Microsoft Agent Framework with capabilities from Azure Durable Functions, enabling you to build AI agents that survive failures, scale elastically, and execute predictably through durable and distributed execution. The extension is built on four foundational value pillars, which we refer to as the 4D’s: Durability Every agent state change (messages, tool calls, decisions) is durably checkpointed automatically. Agents survive and automatically resume from infrastructure updates, crashes, and can be unloaded from memory during long waiting periods without losing context. This is essential for agents that orchestrate long-running operations or wait for external events. Distributed Agent execution is accessible across all instances, enabling elastic scaling and automatic failover. Healthy nodes seamlessly take over work from failed instances, ensuring continuous operation. This distributed execution model allows thousands of stateful agents to scale up and run in parallel. Deterministic Agent orchestrations execute predictably using imperative logic written as ordinary code. Define the execution path, enabling automated testing, verifiable guardrails, and business-critical workflows that stakeholders can trust. This complements agent-directed workflows by providing explicit control flow when needed. Debuggability Use familiar development tools (IDEs, debuggers, breakpoints, stack traces, and unit tests) and programming languages to develop and debug. Your agent and agent orchestrations are expressed as code, making them easily testable, debuggable, and maintainable. Features in action Serverless hosting Deploy agents to Azure Functions (with expansion to other Azure computes soon) with automatic scaling to thousands of instances or down to zero when not in use. Pay only for the compute resources you consume. This code-first deployment approach gives you full control over the compute environment while maintaining the benefits of a serverless architecture. # Python endpoint = os.getenv("AZURE_OPENAI_ENDPOINT") deployment_name = os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME", "gpt-4o-mini") # Create an AI agent following the standard Microsoft Agent Framework pattern agent = AzureOpenAIChatClient( endpoint=endpoint, deployment_name=deployment_name, credential=AzureCliCredential() ).create_agent( instructions="""You are a professional content writer who creates engaging, well-structured documents for any given topic. When given a topic, you will: 1. Research the topic using the web search tool 2. Generate an outline for the document 3. Write a compelling document with proper formatting 4. Include relevant examples and citations""", name="DocumentPublisher", tools=[ AIFunctionFactory.Create(search_web), AIFunctionFactory.Create(generate_outline) ] ) # Configure the function app to host the agent with durable session management app = AgentFunctionApp(agents=[agent]) app.run() Automatic session management Agent sessions are automatically checkpointed in durable storage that you configure in your function app, enabling durable and distributed execution across multiple instances. Any instance can resume an agent's execution after interruptions or process failures, ensuring continuous operation. Under the hood, agents are implemented as durable entities. These are stateful objects that maintain their state across executions. This architecture enables each agent session to function as a reliable, long-lived entity with preserved conversation history and context. Example scenario: A customer service agent handling a complex support case over multiple days and weeks. The conversation history, context, and progress are preserved even if the agent is redeployed or moves to a different instance. # First interaction - start a new thread to create a document curl -X POST https://your-function-app.azurewebsites.net/api/agents/DocumentPublisher/threads \ -H "Content-Type: application/json" \ -d '{"message": "Create a document about the benefits of Azure Functions"}' # Response includes thread ID and initial document outline/draft # {"threadId": "doc789", "response": "I'll create a comprehensive document about Azure Functions benefits. Let me search for the latest information... [Document Draft] # Benefits of Azure Functions\n\n## Introduction\nAzure Functions is a serverless compute service that enables you to run event-driven code without managing infrastructure...\n\n## Cost Efficiency\n- Pay only for execution time\n- No charges for idle resources\n- Automatic scaling reduces over-provisioning...\n\n## Developer Productivity\n- Multiple language support (C#, Python, JavaScript, Java)\n- Integrated development tools and CI/CD...\n\n## Scalability\n- Automatic scaling based on demand\n- Handle millions of requests seamlessly...\n\nReferences: [Azure documentation, serverless computing studies]"} # Second interaction - continue the same thread to refine the document curl -X POST https://your-function-app.azurewebsites.net/api/agents/DocumentPublisher/threads/doc789 \ -H "Content-Type: application/json" \ -d '{"message": "Can you add a section about integration with other Azure services?"}' # Agent remembers the Azure Functions document context and adds the requested section # {"threadId": "doc789", "response": "I've added a comprehensive integration section to your Azure Functions document:\n\n## Integration with Azure Services\n\n### Azure Storage\nSeamless triggers and bindings for Blob Storage, Queue Storage, and Table Storage enable event-driven architectures...\n\n### Azure Event Grid & Event Hubs\nProcess real-time event streams and implement publish-subscribe patterns at scale...\n\n### Azure Cosmos DB\nBuilt-in bindings for document database operations with automatic change feed processing...\n\n### Azure Service Bus\nReliable message processing with enterprise messaging features...\n\n### Azure AI Services\nEasily integrate OpenAI, Cognitive Services, and AI Search for intelligent applications...\n\nThe document now includes this section after the Scalability section. Would you like me to add use cases or deployment best practices?"} Deterministic multi-agent orchestrations Coordinate multiple specialized durable agents using imperative code where you define the control flow. This differs from agent-directed workflows where the agent decides the next steps. Deterministic Orchestrations provide predictable, repeatable execution patterns with automatic checkpointing and recovery. Example scenario: An email processing system that uses a spam detection agent, then conditionally routes to different specialized agents based on the classification. The orchestration automatically recovers if any step fails and completed agent calls are not re-executed. # Python app.orchestration_trigger(context_name="context") def document_publishing_orchestration(context: DurableOrchestrationContext): """Deterministic orchestration coordinating multiple specialized agents.""" doc_request = context.get_input() # Get specialized agents from the orchestration context research_agent = context.get_agent("ResearchAgent") writer_agent = context.get_agent("DocumentPublisherAgent") # Step 1: Research the topic using web search research_result = yield research_agent.run( messages=f"Research the following topic and gather key information: {doc_request.topic}", response_schema=ResearchResult ) # Step 2: Generate outline based on research findings outline = yield context.call_activity("generate_outline", { "topic": doc_request.topic, "research_data": research_result.findings }) # Step 3: Write the document with the research and outline document = yield writer_agent.run( messages=f"""Create a comprehensive document about {doc_request.topic}. Research findings: {research_result.findings} Outline: {outline} Write a well-structured, engaging document with proper formatting and citations.""", response_schema=DocumentResponse ) # Step 4: Save and publish the generated document return yield context.call_activity("publish_document", { "title": doc_request.topic, "content": document.text, "citations": document.citations }) Human-in-the-loop Orchestrations and agents can pause for human input, approval, or review without consuming compute resources. Durable execution enables orchestrations to wait for days or even weeks while waiting for human responses, even if the app crashes or restarts. When combined with serverless hosting, all compute resources are spun down during the wait period, eliminating compute costs until the human provides their input. Example scenario: A content publishing agent that generates drafts, sends them to human reviewers, and waits days for approval without running (or paying for) compute resources during the review period. When the human response arrives, the orchestration automatically resumes with full conversation context and execution state intact. # Python app.orchestration_trigger(context_name="context") def content_approval_workflow(context: DurableOrchestrationContext): """Human-in-the-loop workflow with zero-cost waiting.""" topic = context.get_input() # Step 1: Generate content using an agent content_agent = context.get_agent("ContentGenerationAgent") draft_content = yield content_agent.run(f"Write an article about {topic}") # Step 2: Send for human review yield context.call_activity("notify_reviewer", draft_content) # Step 3: Wait for approval - no compute resources consumed while waiting approval_event = context.wait_for_external_event("ApprovalDecision") timeout_task = context.create_timer(context.current_utc_datetime + timedelta(hours=24)) winner = yield context.task_any([approval_event, timeout_task]) if winner == approval_event: timeout_task.cancel() approved = approval_event.result if approved: result = yield context.call_activity("publish_content", draft_content) return result else: return "Content rejected" else: # Timeout - escalate for review result = yield context.call_activity("escalate_for_review", draft_content) return result Built-in agent observability Configure your Function App with the Durable Task Scheduler as the durable backend (what persists agents and orchestration state). The Durable Task Scheduler is the recommended durable backend for your durable agents, offering the best throughput performance, fully managed infrastructure, and built-in observability through a UI dashboard. The Durable Task Scheduler dashboard provides deep visibility into your agent operations: Conversation history: View complete conversation threads for each agent session, including all messages, tool calls, and conversation context at any point in time Multi-agent visualization: See the execution flow when calling multiple specialized agents with visual representation of agent handoffs, parallel executions, and conditional branching Performance metrics: Monitor agent response times, token usage, and orchestration duration Execution history: Access detailed execution logs with full replay capability for debugging Demo Video Language support The Durable Extension supports: C# (.NET 8.0+) with Azure Functions Python (3.10+) with Azure Functions Support for additional computes coming soon. Get started today Click here to create and run a durable agent Learn more Overview documentation C# Samples Python Samples8.9KViews3likes8CommentsConnect Azure Functions to more services with managed connectors
Azure Functions can already connect to many Azure services through triggers and bindings. With managed connectors, your functions can access about 1,700 connectors across services such as Microsoft 365, Microsoft Teams, Dataverse, SharePoint, OneDrive, and third-party systems. Connector triggers deliver events from these services to your function, while typed connector clients let your code take actions against them. You get this broader integration surface without writing the webhook registration code or managing the OAuth tokens required to connect to each service. Focus on your function's business logic and let Azure Connector Namespace handles the connection. Azure Functions integration with Connector Namespace is currently in public preview. It supports .NET isolated, Python, and Node.js. Review the managed connectors overview for current language, hosting plan, and regional availability. To demonstrate how connector triggers and actions work together, this article follows a .NET sample that automates RFP intake across SharePoint, Azure Content Understanding, and Teams. From an uploaded RFP to Teams notification Consider an organization that receives requests for proposals (RFPs) in a shared SharePoint document library. Someone must read each document, identify the requested capabilities, determine which subject-matter experts should respond, and notify the right team. The automated RFP intake sample turns that process into an event-driven workflow: A customer uploads an RFP to a SharePoint document library. A SharePoint connector trigger invokes an Azure Function when the file is created. The function uses a typed SharePoint connector client to retrieve the file contents. Azure Content Understanding extracts the document’s text and layout. The function applies deterministic rules to identify the customer, required capabilities, and recommended subject-matter experts. The function uses a typed Teams connector client to post the results as an Adaptive Card in a channel. Connector Namespace manages the SharePoint and Teams connections. The function controls file processing, document analysis, routing rules, error handling, and notification content How the sample works The .NET sample demonstrates both parts of the connector programming model: a connector trigger receives an event from SharePoint, and typed connector clients provided by the Connector SDKs to perform actions against SharePoint and Teams. The function starts when the SharePoint When a file is created trigger detects a new RFP. It declares the trigger using the ConnectorTrigger attribute and receives a typed payload containing the file’s properties: [Function("OnNewFile")] public async Task OnNewFile( [ConnectorTrigger] SharePointOnlineOnNewFileItemsTriggerPayload payload, CancellationToken cancellationToken) { // Process the newly uploaded file. } Because the trigger provides file properties rather than its contents, the function uses a typed SharePoint client to retrieve the document: byte[] response = await _sharePoint.GetFileContentAsync( Uri.EscapeDataString(siteAddress), fileIdentifier, cancellationToken: cancellationToken); byte[] document = SharePointFileContent.Decode(response); The SharePoint and Teams clients are registered through dependency injection. Each client uses the runtime URL of its Connector Namespace connection and authenticates with DefaultAzureCredential: services.AddSingleton( new SharePointOnlineClient( new Uri(sharePointRuntimeUrl), credential)); services.AddSingleton( new TeamsClient( new Uri(teamsRuntimeUrl), credential)); The function sends the document to Content Understanding’s prebuilt-layout analyzer, which extracts its text and structure. It then applies deterministic C# rules to identify the customer and required capabilities and map those capabilities to predefined subject-matter expert roles. Finally, the function creates an Adaptive Card containing the results and posts it to the configured Teams channel with the typed Teams client: await _teams.PostCardToConversationAsync( postAs, postIn, request, cancellationToken); Connector Namespace handles the SharePoint and Teams connections, while the function controls the document analysis, routing logic, error handling, and notification content. Try the sample The RFP intake sample includes the function code, Bicep infrastructure, Azure Developer CLI configuration, and supporting scripts. Its README explains how to test the workflow locally and deploy it to Azure. Common connector patterns Managed connectors are useful when a function must react to events or perform operations in external systems. Common patterns include: Event to action: React to an event in one service and take an action in another. Event to enrich to action: Retrieve additional information related to an event before acting. Event to document analysis to action: Extract text and structure from a document, apply application rules, and send the result through another connector. Event to AI to action: Analyze event data with an AI service and write the result back through a connector. Extend an existing function app: Add connector-based integrations alongside HTTP, timer, queue, Service Bus, Event Grid, or Durable Functions workloads. The RFP sample combines several of these patterns. A SharePoint event starts the workflow, a SharePoint action retrieves the document, Content Understanding extracts its contents, application code enriches the result, and a Teams action sends the notification. Closing thoughts Managed connectors extend the external systems that can trigger your functions and the services your function code can act on. This brings services such as SharePoint, Teams, Microsoft 365, and many third-party systems into the Azure Functions programming model without requiring you to build the underlying webhook and OAuth infrastructure. Choose Azure Functions with managed connectors when you want this broader integration surface in a code-first application and need custom branching, application libraries and SDKs, other Functions bindings, document or AI processing, or application-specific logic between the trigger and action. If the workload primarily orchestrates connector operations, involves little custom code, and would benefit from a visual designer, Azure Logic Apps is usually the simpler choice. Resources Documentations Overview of managed connectors in Azure Functions Azure Functions connector samples Azure Connector Namespace overview Content Understanding prebuilt-layout analyzer Connector SDK GitHub repos .NET SDK Python SDK Node.js SDK377Views0likes0CommentsEnable Dynamic Workflows in Azure Functions hosted skills
Azure Functions already gives you a familiar way to build event-driven apps. A queue message, HTTP request, timer, or event triggers the code that handles the work. Azure Functions hosted skills (formerly Serverless Agents) add AI reasoning to that model. A hosted skill can read a request, use the regular tools you give it to inspect context, and choose the next step, while your triggers, tools, and business logic stay in place. When the work needs to keep going Consider an insurance policy servicing request. A hosted skill can use its regular tools to understand the requested change, look up the policy, and inspect the submitted documents. If the information is ready and the request can finish now, the normal tool loop, where the model calls a tool, reads the result, and decides the next step, is a good fit. That changes when the work must continue after the initial request. An insurance policy servicing request may need to inspect several documents in parallel, wait for a configured delay before checking again for missing information, and build a review packet after the checks it depends on complete. In a normal tool loop, each result returns to the model before the skill can decide what happens next. The application must keep the job alive, save its progress, and deliver the final result. At that point, the work needs to keep running independently of the original interaction instead of relying on the model and application to coordinate every step. For a queue or other non-HTTP trigger, the final result also needs to be written or sent somewhere useful because there is no response channel. Introducing Dynamic Workflows Dynamic Workflows brings a programmatic tool-calling pattern to Azure Functions hosted skills. Instead of sending every tool result back to the model so it can decide the next call, the model creates a structured, validated workflow plan once. Durable Functions then executes the allowed workflow-safe tool calls, waits, and subagent tasks, passing intermediate results through the workflow instead of the model context. This can reduce model turns and token use for multi-step work while making the work durable. That separation addresses the limits of the normal tool loop: the workflow store keeps state and intermediate results out of the model's context, independent checks can run in parallel, and durable timers resume waits without holding a worker open. Because a Durable Functions orchestration handles execution, the work can continue after the original request or a Functions worker restart. To test the difference, we ran the same structured multi-step task with the regular tool loop and with Dynamic Workflows, using a Foundry gpt-5.4-mini deployment. We ran it with inputs for one service and then ten services. In the Dynamic Workflows version, the model made the plan once, while the runtime kept intermediate tool results in the workflow store instead of sending them back to the model after every tool call. Dynamic Workflows used 56% fewer total model tokens for the one-service run and 93% fewer for the ten-service run, while producing the same final reports. Results will vary by workload and model, and small jobs can have planning overhead. The savings are largest when intermediate tool results would otherwise return to the model after every tool call. How it works Enable workflows in the hosted skill's Markdown front matter. The runtime then adds the management tools: start_workflow, get_workflow_status, list_workflows, cancel_workflow, and terminate_workflow. You do not implement those tools. You choose the workflow-safe tools and subagents that a plan can use. At run time, the AI model uses the hosted skill's instructions to generate a structured plan, limited to the workflow-safe tools and subagents you explicitly allow. The hosted skill calls start_workflow with that plan; the runtime validates it, starts a Durable Functions orchestration, and returns a workflow ID right away. --- name: Add Driver Review description: Prepares an add-driver document review for an insurance representative. workflows: enabled: true trigger: type: queue_trigger args: queue_name: policy-service-requests connection: AzureWebJobsStorage --- Put workflow-safe handlers under tools/ and decorate them for use in a workflow. Each handler must run synchronously, accept one dict argument, return JSON-serializable data, and be idempotent. A worker failure can cause a handler to run more than once, which is why that last point matters. Ordinary tools retain their existing behavior unless you explicitly make them available to a workflow. workflow_tool( description=( "Inspect one document from an add-driver request. Args: " "{document: <document>, position: int}. Returns the document and evidence state." ) ) def inspect_driver_document(args: dict[str, Any]) -> dict[str, Any]: document = args["document"] evidence_state = { "received": "present", "missing": "missing", "expired": "needs_current_copy", }[document["status"]] return { "position": args["position"], "document_id": document["document_id"], "type": document["type"], "file_name": document["file_name"], "evidence_state": evidence_state, } Dynamic Workflows runs on Durable Functions. You can configure Durable Task Scheduler in host.json and use its dashboard to see per-instance task state, retry history, and controls for work that is still running. Durable timers let a workflow wait without keeping a worker busy, then resume the steps that are ready. The workflow stays visible and durable instead of depending on an open request or a best-effort background task. Get started Build your first Azure Functions hosted skill dynamic workflow with the quickstart, then use the overview and sample to go deeper: Follow the Dynamic Workflows quickstart. Read the Dynamic Workflows overview. Browse the insurance policy review sample for an end-to-end implementation.403Views0likes0CommentsSSL/TLS certificates and end-to-end encryption for Azure Functions Flex Consumption
Azure Functions Flex Consumption now supports site-scoped TLS certificates and end-to-end TLS encryption. Learn how to secure custom domains, certificate-based scenarios, and traffic through the function worker.396Views0likes0CommentsMaking Azure AI Foundry Agents Explainable — Knowledge Graphs + Source Attribution
90% of Azure AI demos work on stage — most never ship. The gap is architecture, not the model. I wrote up a field guide on taking Azure AI Foundry agents from POC to production, with knowledge graphs (GraphRAG) doing the grounding and source attribution. What the post covers Grounding with a knowledge graph — multi-hop retrieval + inline citations so every answer is traceable to a source (critical for regulated/medical use cases). Externalized state — keeping agent memory and session state outside the model. Identity at the boundary — Entra ID / RBAC instead of trusting the prompt. Observability & compliance — tracing, evaluation, and auditability. The stack — Azure AI Foundry + Model Context Protocol (MCP) + Azure Functions (Flex Consumption) + Azure OpenAI, from a real build (VeritasGraph medical MCP server). 📖 Full write-up: https://bibinprathap.com/blog/azure-ai-proof-of-concept-to-production ▶️ 6-min walkthrough: https://youtu.be/z-CPS5WUvyw?si=fmpo1RN28Bh9KbIb Questions for the community How are you grounding Foundry agents today — vector RAG, GraphRAG, or hybrid? Anyone combining knowledge graphs with MCP tools in Foundry? What worked / broke? For regulated domains, how are you handling source attribution and audit trails? Curious to compare notes — happy to share more detail on the GraphRAG retrieval design if useful.309Views0likes0Comments