Skip to content

From 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


Architecture diagram. A permit portal calls the Permits API in Azure API Management, which routes to a Logic App and then to a Service Bus queue with a dead-letter subqueue. An Azure Function extracts fields with Document Intelligence, gets an advisory compliance score from a Microsoft Foundry model through the API Management AI gateway, writes a record through a case-system adapter, and publishes a PermitCreated event to Event Grid, which fans out to a notification webhook and an analytics queue. Application Insights and Log Analytics join intake and processing with one correlation ID.

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.


Diagram of two API surfaces governed by one API Management instance: the Permits API with token validation, rate limiting, and a correlation ID, and the Azure OpenAI v1 API with content safety, token limits, token metrics, and managed identity authentication. The Function’s scoring call routes back through the gateway to a Foundry model.

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.



  1. Submit. The client posts the permit to API Management: { “name”: “[Applicant Name]”, “type”: “Building”, “parcel”: “AIS-TT-102242”,
    “applicantEmail”: “[Applicant Email]” }

  2. 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.

  3. 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.

  4. 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.)

  5. Record and publish. The case-system adapter creates P-E6980B0C, and the Function publishes PermitCreated; Event Grid delivers it to the analytics queue.

  6. Reconstruct. One query returns the whole journey.


The portal shows the same journey to a business audience:


The Permit Intake Portal in simulated mode. An applicant, shown as the placeholder [Applicant Name], submits a building permit and the portal shows the correlation ID, the permit ID, an IntakeReview status, a compliance score of 100 out of 100, a published event, and a seven-row trace table from API Management through the Logic App, Service Bus, Document Intelligence, the AI gateway, the case record, and Event Grid, with an info button on each row.

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.


Application Insights end-to-end transaction for the permit processor, listing each step in order: synthetic field extraction, the compliance-scoring call back through the API Management AI gateway, two Azure AI Content Safety checks, the gpt-5.4-mini model call, a compliance score of 22 with the missing items, the case record, the Event Grid publish, and a final trace line reporting permit P-E6980B0C for parcel AIS-TT-102242 with status NeedsAttention. Resource names and identifiers are redacted.

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

Production landing zone diagram with an edge web application firewall, API Management in a hub virtual network, application workloads in a spoke virtual network, and private endpoints for data, messaging, and AI services.

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



  1. 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.

  2. 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.

  3. Keep AI advisory. Let the model route work to people. Don’t let it make the decision.

  4. Create one correlation ID at the edge and carry it through each hop. It’s one of the simplest audit trails you can build.

  5. Decouple what happens next. Event Grid lets new consumers subscribe without touching the intake flow.


What to do Next



  1. Agree on the acceptance contract: exactly when 202 is returned and how callers check status.

  2. Run the simulated portal and Python walkthroughs with one synthetic correlation ID.

  3. Test the failure paths: queue unavailable, poison message, model unavailable, case system unavailable, and a trace gap.

  4. Add idempotent record writes and an outbox for event publication.

  5. Evaluate the advisory model separately from deterministic rules and human approval.

  6. Complete an architecture, privacy, security, and Well-Architected review for your subscription before using real data.


Try It



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.

Microsoft Tech Community originally posted this article on 9 October 2026 at 11:00 PM.

Leave a Reply