A markdown-first Azure Functions hosted skill for queue-driven expense processing. Its trigger and
instructions live in
src/expense_processor.agent.md, and Azure Functions
handles execution and scale-to-zero.
- 🧾 Reads any format: text, email, key/value, or JSON, and normalizes amounts expressed with symbols, currency codes, words, or colloquial units.
- 📚 Picks the right policy: lists the documents in Blob Storage and selects the one whose scope matches, then reads and applies it through a read-only Connector Namespace MCP server.
- 🛡️ Governs model calls: sends deployed model traffic through an Azure API Management AI Gateway with managed identity, token limits, prompt shielding, harmful-input filtering, and token metrics.
- 🚦 Routes the decision:
approve→expense-approved,review→expense-review,flag/ FX →expense-flagged. - 🔀 Proves it's reasoning: the same normalized 450 USD is auto-approved as travel but sent to review as a client dinner; tighten one policy document and only that category reroutes.
The deployment creates an API Management Developer instance. This is the lowest tier that supports the sample's token-limit and Content Safety policies, but it has a recurring cost and no production SLA.
uv sync --project src
azd auth login
azd upThe deployment seeds the policy documents but does not submit expenses, so a fresh deployment's queues remain empty. Verify the empty output queues:
uv run --project src --no-sync python scripts/read_decision.py --queue all --peek --cloudSubmit the three bundled demo expenses, allow up to two minutes for processing, and read the resulting decisions:
uv run --project src --no-sync python scripts/setup_demo.py send-samples
uv run --project src --no-sync python scripts/read_decision.py --queue all --peek --cloudsend-samples skips submission when an output queue already contains a decision. To reset the
three output queues for a fresh demo, receive and remove their messages, then run send-samples
again:
uv run --project src --no-sync python scripts/read_decision.py --queue all --cloud --max 1000Omitting --peek deletes the messages it reads. This command clears the three output queues; it
does not clear the input or poison queue. The --no-sync commands reuse the environment prepared by
the initial uv sync --project src and do not contact PyPI.
You should then see:
| Request as received | Amount inferred | Policy | Queue |
|---|---|---|---|
| “four hundred and fifty dollars” flight | 450 USD | travel-policy.md |
expense-approved |
USD 450.00 monitor |
450 USD | equipment-software-policy.md |
expense-approved |
$450 client dinner |
450 USD | meals-entertainment-policy.md |
expense-review |
Open the Application Insights overview, then use Search (formerly Transaction search) or Logs to inspect the hosted skill, model calls, and tool spans:
azd monitor --overviewLook for successful execute_tool azureblob_ListFolder_V4,
execute_tool azureblob_GetFileContentByPath_V2, and
execute_tool azurequeues_PutMessage_V2 spans, plus AI Gateway requests and token metrics. See
Deploy for the portal walkthrough.
Clean up with azd down --purge.
Install Azurite and
Azure Functions Core Tools.
Copy src/local.settings.json.sample to src/local.settings.json
and set the model endpoint and deployment. No API key is needed; the runtime uses
DefaultAzureCredential, so sign in with az login or azd auth login. Policy lookup uses the
deployed Connector Namespace because Connector Namespace has no local emulator. After
azd provision, copy
POLICY_MCP_SERVER_URL and QUEUE_MCP_SERVER_URL from azd env get-values into local settings
and leave both MCP client IDs empty so your developer credential is used.
azurite --silent --location .azurite # terminal A
cd src && uv run func start # terminal B
uv run --project src --no-sync python scripts/send_expense.py --file samples/travel.txt # terminal C
uv run --project src --no-sync python scripts/read_decision.py --queue all --peek --cloudThe model call and both Connector Namespace MCP servers still use Azure. The local trigger reads Azurite, but decisions are written to the deployed Azure output queues because Connector Namespace has no local emulator. For setup and Windows help, see Troubleshooting.
flowchart LR
msg([raw message<br/>text · JSON · key-value])
subgraph inbound["Azure Queue Storage · inbound"]
inq[[expense-requests]]
end
subgraph policies["Azure Blob Storage · policies container"]
pdocs[(travel · meals · equipment<br/>general policy docs)]
end
skill{{Expense Processor skill<br/>extract · select · apply · route}}
gateway{{API Management AI Gateway<br/>identity · token limits · content safety · metrics}}
model[(Microsoft Foundry<br/>gpt-5.4)]
subgraph outbound["Azure Queue Storage · outbound"]
approved[[expense-approved]]
review[[expense-review]]
flagged[[expense-flagged]]
end
msg --> inq
inq -->|queue trigger| skill
pdocs -->|Connector Namespace<br/>Blob MCP tools| skill
skill -->|Responses API| gateway
gateway -->|managed identity| model
skill -->|Queue MCP| approved
skill -->|Queue MCP| review
skill -->|Queue MCP| flagged
The runtime discovers the hosted skill's Markdown definition. Its front matter defines the queue trigger, and its body contains the instructions. A read-only Azure Blob MCP connector lists and reads policy documents, while a write-only Azure Queues MCP connector routes decisions to the three output queues. The Connector Namespace managed identity receives container-scoped Blob Reader and queue-scoped Message Sender roles. In Azure, model requests use the Function identity to enter the AI Gateway; API Management then calls Microsoft Foundry with its own managed identity.
How it works · Use cases · Customize · Deploy · Troubleshooting
- Hosted skills in Azure Functions
- Azure Functions Flex Consumption
- Connector Namespace
- Generative AI gateway capabilities
- uv · PEP 723: inline script metadata
MIT © Microsoft Corporation.