Azure Functions or Foundry Agent Service Is the Wrong Question

Enterprise teams keep asking me the same architecture question. Should agent work run in Azure Functions or in Foundry Agent Service? The question sounds like a choice between two products. It is not. It is a question about where you draw the Foundry Agent Service architecture boundary, and the honest answer changed in the last twelve months while most of the diagrams did not.

The Foundry Agent Service architecture boundary, as commonly drawn

Here is the version of the pattern I see repeated in decks, posts, and internal architecture docs.

Azure Functions sits below the line as the action layer. Tools, action handlers, data retrieval, API adapters. Stateless, discrete, testable, independently scalable. Foundry Agent Service sits above the line as the orchestration and reasoning layer. Thread state, tool coordination, evaluation, governance. The banner at the bottom says to keep orchestration in Foundry and actions in Functions.

Most of that diagram is still correct. One box in it is now wrong, and it happens to be the box the whole argument rests on.

The claim that stopped being true

The reasoning behind the pattern went like this. Azure Functions is a stateless runtime. If you build agent orchestration inside it, you end up hand building a state machine: persisting conversation turns yourself, tracking which tool calls already completed, writing your own resume logic after a crash, and owning every retry path end to end. That is real work, it is easy to get wrong, and Foundry Agent Service does it for you. So orchestration belongs above the line.

In 2025 that argument held. Then the Durable Task extension for Microsoft Agent Framework shipped, and Azure Functions acquired the thing it was said to lack.

Durable agents give you persistent sessions that survive process crashes, restarts, and scaling events. They checkpoint automatically, so completed steps do not re-execute on recovery. Sequential, fan out and fan in, and conditional routing across multiple agents all work, with context.GetAgent() pulling a registered agent into an orchestration. And the whole thing runs on the Flex Consumption plan and scales to zero.

That is a managed state machine, in Azure Functions, supported by Microsoft. The strongest paragraph in the received pattern now argues against a product that exists.

Preview status, read from the feed rather than the docs

Note the status carefully, because this is where enthusiasm outruns the facts. The core Microsoft Agent Framework packages are stable. The durable hosting packages are not. Microsoft.Agents.AI.Hosting.AzureFunctions and Microsoft.Agents.AI.DurableTask have never had a stable release. Neither Learn page carries a preview banner, but every documented install command passes --prerelease, and the feed agrees.

Building the sample turned up something sharper than the package labels. Microsoft.Agents.AI.Foundry 1.19.0 binds Azure.AI.Projects 2.1.0.0, and version 2.1.0 exists only as a beta. Pin the stable 2.0.1 underneath it and the compiler refuses with a version conflict. So the maturity question does not stop at the hosting packages. Today, the current Agent Framework Foundry integration cannot be assembled on top of a stable data plane SDK at all.

Say that out loud in an architecture review, and cite the package feed rather than the docs.

Three substrates above the Foundry Agent Service architecture boundary

Once you accept that Functions can orchestrate, the Foundry Agent Service architecture boundary stops being a layer cake and becomes a decision.

Foundry prompt agents. Configuration rather than runtime code. Foundry owns the thread state, the lifecycle, the tracing, the evaluation, and the retry behavior. This is the fastest route to a governed production agent, and it costs you the least code and the least control. When a team wants an agent in front of existing APIs and does not want to run anything, this is the answer.

Durable agents on Azure Functions. Your orchestration, in C#, with checkpointing supplied by the durable runtime. You own the deployment pipeline, the infrastructure as code, the tests, the alerting, and the pager. You get deterministic, inspectable control flow and the same operational tooling as the rest of your estate. When the orchestration has real branching, compensation, or long running steps that must survive a restart, this earns its keep.

Foundry hosted agents. Your Agent Framework or LangGraph code, shipped as a container image or a zip into Foundry’s runtime. Foundry supplies the endpoint, the scaling, a dedicated Microsoft Entra identity per agent, and session level state. You supply the code. This is the case that blurs the boundary the original diagram insists on keeping clean, because neither side owns the whole thing.

One action layer below the line

Below the line, nothing changes. Azure Functions remains the action and tool layer for all three. You surface tools over MCP at the Functions webhook endpoint, register them once in a Foundry toolbox (I covered how that moves tool authentication off the agent in my Foundry Toolboxes post), and let any substrate above discover them at runtime. The tool code does not know or care which orchestrator called it, and hardening it is ordinary API work of the kind I described in Azure Functions behind API Management.

The companion repo, and what it proves

I built a companion repo to check that this holds in practice rather than only in a diagram. It contains one action layer and three orchestrators over it, running the same order fulfillment task. You can find it at function-foundry-boundry.

All three substrates ran against the deployed action layer. The durable agent is deployed and measured, with the smoke suite passing twelve of twelve: the action layer, the idempotency guarantee, and a full orchestration ending in exactly one customer notification. Its prompt agent sibling was created against the live MCP endpoint and driven from the Foundry playground. And the hosted agent is registered and active in Foundry, with its container exercised end to end over the responses protocol self hosted; the one path I have not driven is invoking that container through Foundry’s own runtime. Getting all of this working took forty one documented findings, every one listed in the repo’s verification log, and several of them taught me more than the sample did. The repo states its coverage in the same words, because a sample that overstates what it proved is worse than no sample.

What actually differs, in code

Building all three surfaced a difference I did not expect to be so concrete.

The action layer exposes five operations: look up an order, check inventory, reserve stock, arrange delivery, and notify the customer. Two of those change state a customer can see. Reserving stock commits inventory. Notifying the customer sends a message that cannot be unsent.

In the durable agent, those two operations do not appear in the model’s tool set at all. The orchestrator calls them as activities, where the durable runtime commits them exactly once even if the surrounding agent turns replay. What the model gets is three read oriented tools and the job of producing a plan, while the runtime executes the parts that spend money.

In the prompt agent and the hosted agent, there is no activity boundary to put them behind. The model itself has to be trusted to call each one exactly once, and your protection is instructions plus an idempotency key enforced by the tool.

That asymmetry is the trade, stated in about twelve lines. It is not a bug in either approach. It is what “who owns retry and failure handling” means once you stop discussing it abstractly.

Three substrates, three failures, one shape

I did not have to wait long for the trade to show itself in data. All three substrates ran against the same action layer on the same day, with instructions that said, in each case, to notify the customer exactly once and to use the delivery slot they were given. The prompt agent fulfilled its order correctly and notified the customer twice, in two differently worded messages, because two generations each decided a message was warranted; the reservation, protected by the idempotency key rather than by instructions, did not duplicate. The hosted agent notified exactly once, and booked the delivery for the literal phrase “next available”, because with no orchestrator in the stack nobody owns the clock, so the model passed prose where a date belonged and the action layer recorded it faithfully. And the durable agent, in an early revision that let the model judge whether stock sufficed, refused a fulfillable order with twelve units on the shelf, which is why the shipped version keeps the model out of that decision entirely. Three substrates, three different failures, one shape: whatever the runtime owned, held. Whatever the model was merely instructed about, eventually did not.

One practical note for anyone following the same path. The durable types are split across three namespaces that do not follow from one another: AIAgent, AgentSession and AgentResponse<T> come from Microsoft.Agents.AI, DurableAIAgent and context.GetAgent from Microsoft.Agents.AI.DurableTask, and ConfigureDurableAgents from Microsoft.Agents.AI.Hosting.AzureFunctions. The DurableTask package is documented as the bring your own compute option, which makes it easy to leave out of a Functions project. You need it on both paths, because the hosting package does not surface the orchestration types.

The one control that survives the decision

Which brings up the idempotency key. Every substrate can retry a tool call, and every substrate can replay. So the action layer refuses to care which one is calling and enforces safety itself. reserve_stock takes a reservation key and returns the existing reservation rather than holding stock twice. The key derives from the order, never from the run. A run derived key is indistinguishable from no key at all, because every retry invents a fresh one. I measured that exact failure in an earlier sample, and it produced two authorizations against a balance that only had room for one.

Put the idempotency key in the action layer regardless of which substrate you pick. It is the one control that survives the architecture decision.

What the durable orchestration looks like

The reason this is worth showing rather than describing is that the code contains almost no plumbing. Here is the middle of the fulfillment orchestration, trimmed for length.

[Function(nameof(FulfilOrder))]
public static async Task<FulfilmentOutcome> FulfilOrder(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
var request = context.GetInput<FulfilOrderRequest>();
DurableAIAgent agent = context.GetAgent(AgentName);
AgentSession session = await agent.CreateSessionAsync();
// Facts come from activities, never from the model.
var order = await context.CallActivityAsync<Order?>(
nameof(ToolActivities.LookupOrder), request.OrderId);
if (order is null) { /* unknown order, return */ }
// The key derives from the order, never from the run. And there is no stock
// pre-check: reserve_stock IS the stock check, enforced atomically by the
// action layer, which is the only answer that cannot go stale between
// checking and reserving.
var reservationKey = $"resv-{request.OrderId}";
var reservation = await context.CallActivityAsync<ReservationResult>(
nameof(ToolActivities.ReserveStock),
new ReserveArgs(order.Sku, order.Quantity, reservationKey));
if (reservation.Reservation is null) { /* notify once with the action layer's reason, return */ }
// Only now does the model get a turn: book the slot the orchestrator computed,
// and write the customer message. Checkpointed: on replay this returns from
// history without calling the model.
AgentResponse<FulfilmentPlan> plan = await agent.RunAsync<FulfilmentPlan>(
message: $"Stock is held for {request.OrderId}. Call arrange_delivery with " +
$"the slot exactly as '{slot}'. Then write one short message " +
"telling the customer what happens next.",
session: session);
// ...
}

Count what is absent. There is no thread table, no bookkeeping to work out which steps already ran, no resume logic, and no retry loop. context.GetAgent pulls in an agent registered at startup, each RunAsync is a checkpoint, and CallActivityAsync marks the operations that must commit exactly once.

When the model judged a fact

An earlier revision opened this orchestration with a triage turn: the agent was told to look the order up, check inventory, and decide whether there was enough free stock. It worked all day, and then, with twelve units free and two wanted, it returned canFulfil false with the reason “Free stock is lower than the order quantity”. The tools had been called, and the inventory tool had answered twelve. Yet the model fumbled a comparison a CPU does perfectly, the orchestrator took its word for it, and a customer was told an order could not be fulfilled while eleven spare units sat on the shelf. The division of labour above is the correction: activities fetch facts, the action layer enforces constraints, and the model plans and writes, which is the work only a model can do. It never decides whether.

Registration is equally short. In Program.cs you build an ordinary Agent Framework agent against the Foundry project, then hand it to the durable host.

AIAgent agent = new AIProjectClient(new Uri(foundryEndpoint), new DefaultAzureCredential())
.AsAIAgent(model: modelDeployment, instructions: Instructions, name: AgentName,
tools: [ /* the read oriented tools only */ ]);
builder.ConfigureDurableAgents(options => options.AddAIAgent(agent));

Compare that against what the same behavior cost in 2025, and you can see why the received pattern needs updating rather than repeating.

What deploying it actually cost

I would not have found any of the following by reading documentation, and three of them would sink a production build.

The preview and stable halves of one product are on different trains. Microsoft.Agents.AI is stable at 1.19. Microsoft.Agents.AI.DurableTask and Microsoft.Agents.AI.Hosting.AzureFunctions are preview at 1.16. The durable packages hook into internals of the core package, nothing declares an upper bound, and NuGet pairs them without a word. It restores, compiles, deploys and starts. The agent answers in eighteen seconds. Then the orchestrator fails a determinism check, gets retried forever, and reports Running until you go looking. No compiler error, no restore warning, no startup error.

The fix is to pin the whole family to the older train, which means pinning the stable package down to match the previews. That is worth saying out loud in a review, because it is backwards from how anyone manages versions.

Failures that look like working systems

Two Azure Functions apps must not share a deployment container. Flex Consumption keeps the deployed package in the container you name. Point two apps at the same one and the second deployment overwrites the first, so an app runs the other app’s code. It does not fail. The host starts cleanly, registers whatever it found, and every route you wrote returns 404. The only reliable tell is comparing the registered function list against what you wrote.

A client that turns failures into nulls defeats the whole point of a durable activity. My first tool client returned null on any non-success status. When a function key went missing, the agent received “no such order” and reasoned confidently about a fabricated premise, while the notification activity returned success having done nothing. Exactly-once is only a guarantee if a failed call is allowed to look like one. An activity that swallows a transport error is worse than one that crashes, because neither the runtime’s retry nor the orchestration’s error handling ever engages.

Two smaller ones worth carrying. In-process state in the action layer is simply wrong on a platform that scales out, and moving reservations to Table Storage made the idempotency key stronger than the lock it replaced, because a duplicate row key returns 409 atomically across every instance. And the delivery date belongs in the orchestrator, not the prompt: an orchestrator must be deterministic, so time comes from the replay-safe clock. Ask a model for “the earliest weekday morning slot” and you will get a booking reference containing the literal phrase, which is funny once and then a determinism bug forever.

Where the platform enforces the Foundry Agent Service architecture boundary

One thing I expected to argue for turned out not to need arguing. Flex Consumption allows exactly one site per plan, so the action layer and the orchestrator cannot share one. The received diagram calls the action layer “independently scalable” as a design aspiration you could honour or skip. On this platform you have no choice: the two sides of the boundary scale on separate plans by construction and cannot starve each other of instances.

That is a better argument for drawing the line than any of mine, because it is a constraint rather than an opinion.

The four questions that settle it

When teams argue about this, they argue about product features. The argument ends faster if you ask four operational questions instead.

Who owns retry and failure handling when a tool call dies? With a prompt agent, Foundry does. With a durable agent, you do, and you decide the policy. And finally, with a hosted agent, it splits, and the split is the thing to write down.

Where does session state live, and in whose subscription? All three substrates support bring your own resources for conversation state, using Azure Storage, Cosmos DB, or Azure AI Search inside your own subscription and under your own retention policy. This matters enormously for data residency, and it is also why residency alone cannot pick a substrate for you. It used to be the deciding argument. It is now available on every path.

Which control plane does the on call engineer open at three in the morning? This question ends more debates than the other three combined. If the answer is “it depends which agent”, you have not drawn a boundary, you have accumulated one.

Is the topology documented, or improvised per team? Teams that struggle operationally are rarely the ones that picked the wrong substrate. They are the ones that picked different substrates for different agents without recording why, and inherited the failure modes of both with the runbook of neither.

Where the Foundry Agent Service architecture boundary is the wrong frame

I have made this sound tidier than it is. Several situations break the framing above.

When the orchestration is genuinely trivial. A single agent, three tools, no branching, no long running steps. Reaching for a durable orchestrator here buys you preview packages, a Durable Task backend, and a deployment pipeline in exchange for checkpointing you will never exercise. Use a prompt agent and move on.

When your team has no .NET or Python depth. The durable path is code. It rewards a team that already runs Functions, already has infrastructure as code, and already reviews pull requests. It punishes a team that wanted an agent and got a distributed systems project.

Constraints that make the decision for you

When the preview status is disqualifying. Some regulated programs cannot take a dependency on packages with no stable release, whatever the docs imply. That constraint is legitimate and it makes the decision for you. Prompt agents and hosted agents are generally available. Say so in the review and let the constraint do its work.

Do not soften this one. The train skew above is exactly the class of failure that preview status is warning you about, and it cost most of a day to find in a sample with four files of orchestration.

When you actually need multi agent collaboration across organizational boundaries. The Agent2Agent protocol appears in a lot of diagrams, including the one at the top of this post, positioned as a settled capability alongside thread state and tool coordination. It is still preview. Design for it if you like, but do not commit a program to it this quarter.

When you are migrating rather than starting. If you are on Agents classic, the deprecation deadline is doing your planning for you. Classic retires on 31 March 2027, and the migration guide makes clear it is not a rename. That is not far away for an enterprise program with a change freeze and a release train.

Draw the Foundry Agent Service architecture boundary before anyone writes code

The original pattern got the important thing right. There is a boundary, it matters, and architectures fail when nobody draws it. What it got wrong was assuming the Foundry Agent Service architecture boundary always falls in the same place.

It does not. You now have three legitimate substrates above the line and one action layer below it. The action layer stays where it is, and the tools you write for it survive every one of these decisions, which is a good reason to build that layer first and pick the substrate second.

Pick one orchestration substrate per workload. Write down which one and why. Then let the four questions above, rather than a diagram, decide the next one.

Building RAG Pipelines with Azure Functions: Event-Driven Data Retrieval at Scale

This is the fifth and final post in the series on AI and Azure Functions. The first post mapped all four AI-enabled patterns. Posts two through four went deep on MCP server hosting, the serverless agents runtime, and Durable Functions for directed agentic workflows. This post covers the fourth pattern: retrieval-augmented generation.

RAG is where Azure Functions earns its place through a capability that is easy to underestimate: the ability to handle multiple events from multiple data sources simultaneously. A RAG pipeline is only as good as its retrieval layer, and retrieval latency in production comes down to two things: how fast you can query your data sources, and how fast you can scale when demand spikes. Azure Functions on Flex Consumption handles both.

What RAG on Azure Functions actually looks like

The Microsoft Learn documentation is deliberately brief on this pattern: “RAG systems require fast data retrieval and processing. Functions can interact with multiple data sources simultaneously and provide the rapid scale required by RAG scenarios.”

That one-liner contains three architectural decisions worth unpacking.

Multiple data sources simultaneously. A retrieval layer that queries one source at a time is a bottleneck. A function app can fan out — trigger parallel calls to Azure AI Search, a Cosmos DB vector store, a blob-indexed knowledge base, and a case history API — collect all results, and pass the aggregated context to the language model in a single call. This is the fan-out/fan-in pattern from the Durable Functions post, applied to retrieval rather than to a directed workflow.

Event-driven. The most common RAG pattern in tutorials is synchronous: the user sends a query, retrieval runs, and the LLM responds. But production RAG often has an asynchronous dimension — new documents arrive, get processed, get indexed. Azure Functions handles both sides: HTTP-triggered retrieval for the synchronous path, and blob/Event Hubs/Cosmos DB change feed triggers for the ingestion pipeline that keeps the index current. The same function app can serve both.

Rapid event-driven scaling. The Azure OpenAI binding extension adds stateful chat session support — the function maintains conversation history across turns without you writing session management code. Combined with Flex Consumption’s scale-to-zero billing, that means a RAG-enabled chat endpoint costs nothing when idle and scales in seconds when traffic arrives.

The Azure OpenAI binding extension

The binding extension is the most concrete piece of infrastructure the docs call out for RAG. It provides three bindings relevant to RAG scenarios:

Text completion input binding — calls an Azure OpenAI deployment and returns the response. Used for one-shot prompt/response patterns.

Chat completion input binding — maintains a stateful chat session across function invocations. The session history is stored externally (Azure Table Storage by default) and automatically injected into each prompt. This is what the “Custom chat bot” sample in the docs demonstrates.

Embeddings input binding — generates vector embeddings from text. Used in the ingestion pipeline to embed documents before writing them to a vector store.

A simple RAG function using the chat completion binding looks like this in C#:

[Function(nameof(RagChat))]
public static async Task<IActionResult> RagChat(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req,
[TextCompletion(
"{query}",
Model = "%OPENAI_DEPLOYMENT%",
SystemPrompt = "You are a helpful assistant. Answer using only the context provided."
)] TextCompletionResponse completion)
{
return new OkObjectResult(completion.Content);
}

The binding handles the Azure OpenAI call, authentication via managed identity, and response parsing. You write the retrieval logic — what context to inject into {query} — and the binding handles the LLM call.

The production version adds a retrieval step before the binding executes. In practice, this means the HTTP trigger extracts the user query, a call to Azure AI Search (or your vector store of choice) retrieves relevant passages, those passages are injected into the prompt template, and then the binding submits the augmented prompt to the LLM.

APIM as the LLM gateway layer

The binding extension simplifies the function code, but it routes directly to Azure OpenAI — there is no governance layer between the function and the model by default. For production RAG pipelines, that matters: you need rate limiting per client, token quota management, cost attribution across tenants or use cases, and a single point for monitoring and logging LLM calls.

APIM fills that role. Place it between the function app and Azure OpenAI, configure an inbound policy to validate the request and apply per-subscription token quotas, and configure an outbound policy to log usage. The function app calls the APIM endpoint rather than the Azure OpenAI endpoint directly.

One practical constraint from real deployment experience: if you use the Azure AI Foundry Agent Service SDK alongside Azure Functions, the SDK routes LLM calls directly to Azure OpenAI and bypasses APIM entirely. The standard OpenAI SDK doesn’t behave this way—calls flow through APIM as expected. The Citadel Platform series covers this in detail, including how to structure APIM policies for the token quota patterns that matter in production.

For RAG pipelines specifically, two APIM policies are worth implementing:

Token quota by subscription key. Set a per-day or per-hour token budget for each API consumer. RAG queries are often significantly larger than simple completions because the retrieved context inflates the prompt — a query with 2,000 tokens of retrieved passages consumes far more quota than a direct LLM call. Factor this into your quota design.

Semantic caching. APIM’s semantic caching policy stores LLM responses and returns cached results for semantically similar queries. For a RAG pipeline serving a knowledge base that changes infrequently, this can dramatically reduce token consumption and latency.

The ingestion pipeline

The retrieval side of RAG gets most of the attention, but the ingestion pipeline- how documents get into the index is where Azure Functions adds less-discussed value.

When a new document arrives in blob storage, the function extracts text, generates a vector embedding, and writes the result to Azure AI Search. An Event Hubs trigger handles high-throughput ingestion where many documents arrive simultaneously. A Cosmos DB change feed trigger keeps a vector index in sync with a transactional database.

The key advantage over a dedicated ingestion service is that the same managed identity, Application Insights workspace, and azd deployment pipeline cover both retrieval and ingestion. Operational complexity stays low even as the pipeline grows.

One constraint to know before building: blob triggers on Flex Consumption require EventGrid as the source. Standard polling blob triggers are not supported on the Flex Consumption plan. This means you must configure an EventGrid system topic and event subscription after provisioning — a two-step post-provision operation that cannot be included in the same Bicep deployment as the function app.

A minimal ingestion function using the Azure OpenAI SDK for embeddings — the recommended approach until the embeddings binding extension reaches GA:

[Function(nameof(IngestDocument))]
public async Task Run(
[EventGridTrigger] EventGridEvent eventGridEvent,
CancellationToken cancellationToken)
{
// Read blob from storage, generate embedding, write to Azure AI Search
var blobContent = await ReadBlobAsync(eventGridEvent.Subject, cancellationToken);
var embeddingClient = openAiClient.GetEmbeddingClient(embeddingsDeployment);
var embedding = await embeddingClient.GenerateEmbeddingAsync(
blobContent, cancellationToken: cancellationToken);
var document = new SearchDocument { ContentVector = embedding.Value.ToFloats().ToArray() };
await searchClient.IndexDocumentsAsync(
IndexDocumentsBatch.Upload([document]), cancellationToken: cancellationToken);
}

The EventGrid trigger fires on MicrosoftStorage.BlobCreated events; the function reads the blob, generates the vector, and writes to the index. EventGrid delivery includes retry logic —failed events are retried with exponential backoff for up to 24 hours.

Connecting the four patterns

This is the final post in the series, so it is worth mapping how the four AI patterns relate to each other in a production architecture. They are not mutually exclusive.

A realistic enterprise RAG system might use all four:

  • Azure Functions RAG pipeline handles retrieval — HTTP-triggered queries fan out to multiple sources, ingestion functions process new documents via blob and Event Hubs triggers.
  • MCP binding extension exposes the retrieval function as an MCP tool, so AI agents and Copilot can call it directly
  • Durable Functions orchestrates multi-step retrieval workflows, including approve-before-index, parallel ingestion with fan-out, andandlong-running document processing with human-in-the-loop review.
  • Serverless agents runtime wraps the whole thing in an agent that decides which data sources to query based on the user’s intent, using the retrieval function as one of its tools

APIM sits in front of the Azure OpenAI calls regardless of which pattern makes the LLM request. The function app’s managed identity authenticates to both APIM and the downstream Azure services. Application Insights provides a unified view across all four patterns.

The spectrum from the Durable Functions post applies here too: RAG retrieval is mostly directed (the function knows which sources to query), but the agent layer that decides what to retrieve and how to use the results is autonomous. Azure Functions provides the event-driven, scalable retrieval infrastructure for both ends of that spectrum.

What to know before building

Choose your vector store before choosing your binding. The Azure OpenAI binding extension integrates cleanly with Azure AI Search. If you are using Cosmos DB for MongoDB vCore, a Postgres pgvector extension, or an external vector database, you write the retrieval logic directly using the SDK — the binding handles the LLM call, not the retrieval.

The embeddings binding and the chat completion binding have different cost profiles. Embeddings are cheap per token; completions are expensive. The ingestion pipeline (embeddings) can run at high volume with low cost. The retrieval + completion path (chat) is where token costs accumulate. Design your APIM quota policies accordingly.

Stateful chat sessions need external storage. The chat completion binding stores session history in Azure Table Storage by default. For multi-tenant RAG, partition session keys by user or tenant — the default key scheme does not enforce isolation automatically.

Retrieval quality determines output quality. Azure Functions handles the retrieval infrastructure; what you retrieve is your responsibility. This distinction matters more than it might seem.

With keyword search alone, a query like “List all Metallica albums” retrieves documents based on term frequency across the index. An index containing Radiohead (48 releases) and Porcupine Tree alongside Metallica will surface Radiohead and Porcupine Tree documents ahead of Metallica ones, because those bands have more indexed content matching common terms. The LLM then filters the irrelevant context and may miss albums that simply did not appear in the retrieved passages.

Hybrid search combining keyword retrieval with vector retrieval addresses this directly. Generate an embedding for the user query, add a VectorizedQuery targeting the content_vector field, and Azure AI Search fuses both signals using reciprocal rank fusion. Semantically relevant documents surface regardless of term frequency:

var vectorQuery = new VectorizedQuery(queryVector)
{
KNearestNeighborsCount = 20,
Fields = { "content_vector" }
};
var searchOptions = new SearchOptions
{
Size = 20,
VectorSearch = new VectorSearchOptions { Queries = { vectorQuery } }
};
// Hybrid: keyword query string + vector query sent together
var results = await searchClient.SearchAsync<MusicDocument>(
userMessage, searchOptions, cancellationToken);

The companion repo implements hybrid search in MusicChatAgent.cs. Chunk size, embedding model choice, and whether you use keyword, semantic, or hybrid search have more impact on output quality than any infrastructure decision.

Testing the RAG pipeline

The companion repo supports several query patterns that exercise different aspects of the pipeline. These are worth running after ingesting a few bands to verify both the retrieval and the grounding behaviour.

Artist-specific ranking — tests hybrid search surfacing the right band:

{"message": "List all Metallica albums with their ratings, ordered highest to lowest"}

Date filtering via LLM reasoning — the index has no date filter; the model reasons over retrieved content:

{"message": "What albums did Tool release in the 1990s?"}

Cross-band comparison — tests whether retrieval surfaces relevant documents from multiple bands:

{"message": "Compare Opeth and Porcupine Tree — which has more critically acclaimed albums?"}

Rating threshold query:

{"message": "Which albums across all bands have a rating above 4.5?"}

Not-in-index grounding test — the agent should say it does not have information rather than hallucinate:

{"message": "Tell me about a band called Coldplay"}

Validation — should return HTTP 400:

{"message": ""}

The hybrid search implementation means artist-specific queries return only that artist’s documents. With keyword-only search, a query for Metallica in an index containing Radiohead (48 releases) would return mostly Radiohead documents — the vector component ensures semantic relevance wins over term frequency.

The companion repo — Music RAG Agent

The music-rag-agent companion repo implements the full two-pipeline RAG system described in this post. It scrapes band and album data from SputnikMusic, generates embeddings with the Azure OpenAI SDK, indexes into Azure AI Search, and exposes an HTTP chat endpoint that retrieves relevant passages and returns grounded answers.

After ingesting Tool, Opeth, Porcupine Tree, and Radiohead you can ask questions like:

“Which band has the highest rated albums — Tool, Opeth, Porcupine Tree or Radiohead?”

The retrieval correctly surfaces Sputnik ratings and vote counts for each album. The ingestion pipeline handles special characters in album titles (Ænima, Opiate²) via URL-safe Base64 document keys, and the EventGrid trigger ensures reliable delivery with 24-hour retry backoff.

Known Sputnik artist IDs: Tool = 83, Opeth = 932, Porcupine Tree = 328, Radiohead = 86.


Series wrap-up

This post completes the series on AI and Azure Functions. The five posts covered:

  1. The four AI-enabled patterns and how to choose between them
  2. Hosting MCP servers — binding extension (GA), self-hosted SDK (preview), and queue-based tools
  3. The serverless agents runtime — markdown agents, the three configuration files, and dynamic workflows
  4. Durable Functions for directed agentic workflows — five patterns with working C# samples verified on Azure
  5. RAG pipelines — event-driven retrieval, the Azure OpenAI binding extension, and APIM governance

The companion code repositories are at github.com/steefjan1.

Hosting MCP Servers on Azure Functions: GA vs. Preview

The Model Context Protocol (MCP) has become the standard way AI agents and models interact with external systems. If you are building something on Azure and need to expose tools to an AI client, the question is no longer whether to use MCP; it is which hosting option to choose.

Azure Functions supports three distinct approaches. Two of them host MCP servers directly. The third uses message queues instead of MCP calls altogether. Each reflects a different set of trade-offs. This post maps them out so you can pick the right one before you write any code.

Why use Azure Functions for hosting MCP servers?

The doc makes the case concisely: Functions scales efficiently to handle demand and provides binding extensions that simplify AI integration. Both matter for MCP hosting specifically.

MCP servers are called on demand. An AI client sends a request, the server responds, and then goes quiet. That is a bursty, event-driven pattern. Flex Consumption and Elastic Premium both handle it well. Flex Consumption gives you scale-to-zero billing for bursty tool workloads. Elastic Premium gives you pre-warmed instances, predictable latency, and VNet integration for tools that need to reach internal APIs and databases.

Managed identity is the other key piece. Your MCP server likely needs to call downstream Azure services: blob storage, Cosmos DB, and Azure AI Search. With managed identity, you do not need to manage credentials inside your function code, and you do not need to pass secrets through your MCP configuration.

The three ways to host MCP servers on Azure Functions

Option 1: MCP binding extension (GA)

This is the right default for most teams. The binding extension lets you build an MCP server using the standard Functions programming model triggers, bindings, local development with Core Tools, and deployment via azd. You annotate a function with McpToolTrigger, and it is automatically exposed as an MCP tool.

The comparison table from the Microsoft Learn documentation makes the feature set clear:

FeatureMCP binding extension
Support levelGA
Programming modelFunctions, triggers and bindings
Stateful executionSupported
LanguagesC# (isolated), Python, TypeScript, JavaScript, Java
Other requirementsNone
ImplementationMCP binding extension

Stateful execution support is the binding extension’s meaningful advantage over the self-hosted SDK option. If your MCP server needs to maintain session state across tool calls, for example, a multi-turn data retrieval scenario, only the binding extension handles that today.

The quickstart template (remote-mcp-functions-dotnet) is a useful starting point. It scaffolds the project via azd init, runs locally with the Azurite storage emulator, and includes a .vscode/mcp.json file that wires the local endpoint directly into GitHub Copilot’s agent mode for testing. That local-to-Copilot loop is genuinely quick; you can test a tool call from Copilot chat without deploying anything.

Before you build: a toolchain issue to know about

There is a confirmed bug in Microsoft.Azure.Functions.Worker.Sdk (all versions to 2.0.7) where the auto-generated WorkerExtensions.csproj is hardcoded to net6.0. The MCP extension package requires net8.0, so the build fails out of the box with NU1202: Package is not compatible with net6.0. The Microsoft Learn quickstart does not mention this.

The workaround that actually works has three parts: add an extensionBundle to host.json so the host loads MCP support at runtime rather than through the build-time generator; mark all extension PackageReference entries with PrivateAssets="all" so their build targets don’t re-trigger the generator; and use 1.0.0-preview.3 rather than 1.0.0 or later preview versions, because the generated project still targets net6.0 and needs a package version it can restore against that framework. The companion repo has all three changes applied and a detailed explanation in the README.

The McpToolTrigger attribute is the core mechanism. Here is what the minimal C# version looks like from the quickstart:

[Function(nameof(SayHello))]
public string SayHello(
[McpToolTrigger(HelloToolName, HelloToolDescription)] ToolInvocationContext context
)
{
logger.LogInformation("C# MCP tool trigger function processed a request.");
return "Hello I am MCP Tool!";
}

The trigger attribute registers the function as an MCP tool and handles the protocol negotiation. You write the tool logic; the extension handles the MCP wire format.

Use this when: you are building a new MCP server and do not have an existing codebase using MCP SDKs. It is the fastest path from zero to a deployed, governed MCP server on Azure.

Avoid it when: you already have an MCP server built with an official MCP SDK and want to lift it into Azure without rewriting it in the Functions model.


Note: Microsoft has samples for this option. You can find all the MCP extension samples at aka.ms/remote-mcp

Option 2: Self-hosted MCP servers via official MCP SDKs (preview)

This option lets you take an existing MCP server built with the official MCP SDKs and host it on Azure Functions without rewriting it as a binding-extension-style function app. Functions acts as the hosting runtime; your MCP SDK code runs via custom handlers.

FeatureSelf-hosted MCP servers
Support levelPreview
Programming modelStandard MCP SDKs
Stateful executionNot currently supported
LanguagesC# (isolated), Python, TypeScript, JavaScript, Java
Other requirementsStreamable HTTP transport
ImplementationCustom handlers

Two constraints define whether this option is viable for you right now.

  • First: Streamable HTTP transport is required. The self-hosted option does not support Server-Sent Events (SSE) transport. Suppose your existing MCP server relies on SSE, which many early implementations did; you need to migrate to Streamable HTTP before hosting on Functions. That is a non-trivial change for an established codebase.
  • Second: stateful execution is not supported during preview. If your MCP server is stateless most tool servers are this is not a problem. But if you need session continuity across calls, stay with the binding extension.

The documentation also flags that configuration details for self-hosted MCP servers change during the preview period. That means operational overhead: you may need to adjust configuration as the feature evolves. Factor that into a production timeline.

The sample (Weather server) shows the self-hosted pattern working correctly for a stateless tool. It is a useful reference for understanding the custom handler wiring, but it does not represent a feature-complete production deployment yet.

Use this when: you have an existing MCP server built with official MCP SDKs, it uses Streamable HTTP transport, it is stateless, and you want to run it on Functions without adopting the binding extension model.

Avoid it when: you need stateful execution, you rely on SSE transport, or you are building from scratch. In those cases, the binding extension is the better path.

Note: There is another approach for hosting official MCP SDK-based servers. This approach is about hosting MCP in Connector Namespace, which was announced in public preview at Build.


Option 3: Queue-based Azure Functions tools

This is the option that gets overlooked because it does not follow the MCP pattern at all, and that is sometimes exactly what you want.

Instead of an AI agent calling your function via the MCP protocol, the agent sends a message to a queue (Service Bus or Azure Storage Queue), and a queue-triggered function picks it up asynchronously. Foundry provides Azure Functions-specific tooling for this pattern.

The Microsoft Learn documentation lists the scenarios where this is the right choice:

  • Reliable message delivery and processing
  • Decoupling between AI agents and function execution
  • Built-in retry and error handling
  • Integration with existing Azure messaging infrastructure

That last point is often what decides it in enterprise contexts. If your integration platform already uses Service Bus for order processing, claims workflows, or event-driven pipelines, the queue-based tool pattern slots into that infrastructure without adding a new protocol layer. Your AI agent becomes another message producer; your function is another consumer. Operations teams already know how to monitor and manage it.

The decoupling is also genuinely useful for long-running tool operations. An MCP call is synchronous from the agent’s perspective: it sends a request and waits for a response. A queue-based call lets the agent fire and move on; the function processes in the background and the result arrives via a separate callback or polling mechanism. For operations that take seconds or minutes document processing, batch retrieval, report generation that asymmetry matters.

Use this when: the tool operation is long-running, you need reliable delivery with built-in retry, you are integrating with existing Azure messaging infrastructure, or you want to decouple the agent from function execution timing.

Avoid it when: the agent needs a synchronous response immediately, or you are connecting to AI clients that expect the MCP protocol specifically.

Note: For long-running processes or multi-step workflows, you might also want to consider Durable Functions instead of “vanilla” Functions. Durable Functions provides built-in state persistence and automatic retries, which is helpful when long-running or multi-step workflows fail mid-execution because of things like unreliable network connectivity. Because state is persisted during execution, Durable Functions can rebuild local state up to the point of failure and continue executing from there instead of from scratch. 


How to choose

SituationOption
Building a new MCP server from scratchBinding extension
Need stateful tool executionBinding extension
Have an existing SDK-based MCP server (Streamable HTTP, stateless)Self-hosted SDK
Need async, decoupled, fault-tolerant tool executionQueue-based
Integrating with existing Service Bus infrastructureQueue-based
Building for GitHub Copilot or VS Code agent modeBinding extension
Production deadline in the next quarterBinding extension or queue-based (avoid preview)
I need pre-warmed instances and predictable latencyElastic Premium plan

The preview constraint is the most important practical point here. If you are building for production and your timeline does not allow for configuration changes mid-project, self-hosted MCP servers are not ready. The binding extension and queue-based options are both GA and stable.

APIM as the governance layer

Whichever option you choose, consider putting APIM in front of your MCP server endpoint. This gives you rate limiting, authentication policy, token quota management, and a single point for logging and monitoring across all MCP tool calls.

One practical caveat from real deployment experience: the Azure AI Foundry Agent Service SDK routes LLM calls directly to Azure OpenAI, bypassing APIM entirely. Tool calls to your MCP server do pass through APIM, but model calls do not unless you use the standard OpenAI SDK instead. The Citadel Platform series covers this in detail, including how to structure your APIM policies to handle the tool-call traffic that does flow through it.

What comes next

The next post goes deeper into the serverless agents runtime, the preview programming model that lets you define event-triggered agents as function apps, with .agent.md files, agents.config.yaml, and remote MCP server connections declared in mcp.json.

Up next: The Azure Functions Serverless Agents Runtime: What It Is and When to Use It

Azure Functions AI Integration: The Quiet Powerhouse

When people talk about running AI workloads on Azure, the conversation usually lands on Azure AI Foundry, Azure OpenAI Service, or maybe Azure Container Apps. Azure Functions tends to get mentioned as the glue the thing you bolt on to handle a webhook. But this overlooks the real story: Azure Functions AI integration has quietly evolved from simple glue into a powerhouse for running production-grade AI.

That framing is outdated. Azure Functions is now a first-class runtime for AI workloads, with four distinct patterns that the Microsoft Learn documentation lays out explicitly. This post walks through each of them and helps you decide which one fits your situation.

The four AI-enabled scenarios

Microsoft groups Azure Functions AI integration into four scenarios:

  1. Serverless agents runtime — event-driven agents that run on serverless infrastructure
  2. Tools and MCP servers — hosting remote Model Context Protocol servers and AI tools
  3. Agentic workflows — multistep, long-running directed agent operations via Durable Functions
  4. Retrieval-augmented generation (RAG) — fast, parallel data retrieval for knowledge-augmented AI

These are not just marketing buckets. Each one reflects a different architectural decision. Let me unpack them.

Azure Functions AI integration: Serverless agents runtime

The serverless agents runtime is a preview programming model for building event-driven agents as function apps. Moreover, Agents are defined in .agent.md files, app-wide runtime defaults live in agents.config.yaml, and remote MCP server connections are listed in mcp.json. The runtime discovers these files, registers the required triggers and endpoints, and runs the agent through the Microsoft Agent Framework when an event fires.

That is a meaningfully different model from what you get in Azure AI Foundry Agent Service. In Foundry, the managed service hosts and orchestrates your agents. In addition, in the serverless agents runtime, your function app is the agent host running on Flex Consumption, with built-in managed identity, monitoring, and scale-to-zero. Furthermore, you write custom Python tools for app-specific logic, and the platform wires in MCP-enabled connections based on Azure connectors and remote MCP servers.

Use this when: you want agents triggered by events, schedules, messages, or HTTP requests and you need the familiar Functions deployment and hosting model rather than a managed agent service.

Avoid it when: you need a fully managed, enterprise-grade agent service with built-in tooling and long-term Microsoft support guarantees today. That is what Foundry Agent Service is built for.

Azure Functions AI integration: Tools and MCP servers

The Model Context Protocol (MCP) has become the industry standard for how AI models and agents interact with external systems. Azure Functions has first-class support for hosting remote MCP servers, and this is already generally available.

There are two hosting options:

OptionStatusHow it works
MCP binding extensionGAUses Functions triggers and bindings; supports stateful execution
Self-hosted MCP servers (MCP SDK)PreviewUses standard MCP SDKs via custom handlers; requires Streamable HTTP transport

The binding extension is the right default. It supports C#, Python, TypeScript, JavaScript, and Java, and it integrates with the Functions programming model you already know. Self-hosted MCP servers offer portability: you can use the official MCP SDKs and bring in existing server code. However, stateful execution is not yet supported, and the configuration is still changing during preview.

There is also a third option worth knowing about: queue-based Azure Functions tools, where AI agents interact with your code through message queues rather than direct MCP calls. Microsoft Foundry provides specific Azure Functions tooling for this pattern. It is ideal when you need reliable delivery, built-in retry, and decoupling between agent and function execution.

Use MCP servers when: you are exposing tools to AI clients and you want the industry-standard protocol with serverless hosting.

Use queue-based tools when: you need asynchronous, fault-tolerant communication between an agent and your function code.

Agentic workflows with Durable Functions

Not all AI orchestration should be autonomous. Some scenarios need predictable, directed steps and that is where Durable Functions fits.

The Microsoft Learn documentation makes the distinction clearly: Durable Functions is positioned as the runtime for directed agentic workflows, not for emergent agent reasoning. Think of it this way: when you know the sequence of steps and you need fault tolerance, auditability, and long-running execution, Durable Functions is the right tool. When you want a model to figure out the steps dynamically, you want an agent runtime.

The documentation gives a clean example: a trip planning workflow that gathers user requirements, searches for options, waits for approval, and makes bookings. Each step is a function; Durable Functions coordinates them with built-in retry, state persistence, and human-in-the-loop support.

Use this when: your AI-driven process has well-defined, ordered steps,authorization flows, multi-stage approval chains, or orchestrated data pipelines where you cannot afford unpredictable execution paths.

Avoid it when: you want a model to determine steps dynamically. That is the serverless agents runtime or Foundry Agent Service territory.

RAG with Azure Functions

Because Functions handles multiple events from various data sources simultaneously, it scales well for real-time AI scenarios, particularly RAG systems where fast, parallel retrieval is the bottleneck.

The Azure OpenAI binding extension lets you integrate RAG directly into your function code. Functions can pull data from multiple sources simultaneously, feed it through Azure AI Search or other retrieval layers, and pass the results to your language model, all within the event-driven, scale-to-zero model that keeps costs down when load is low.

The Azure Functions RAG pattern also pairs naturally with APIM, which handles routing, rate limiting, and token quota management a pattern the Citadel Platform series covers in detail, including the discovery that the Foundry Agent Service SDK bypasses APIM for LLM calls.

Use this when you have event-driven retrieval requirements new documents arriving in blob storage, database change feeds, or streaming IoT data that needs to inform model responses.

How the scenarios relate to other Azure services

It helps to think of Azure Functions AI integration as filling the compute and integration layer between your AI services and your data sources. Here is roughly how that maps:

  • Azure AI Foundry Agent Service — fully managed agent orchestration with enterprise security and built-in tools. Functions integrates into Foundry via MCP servers and queue-based tools.
  • Azure Logic Apps — low-code orchestration for business process automation. Functions is the right choice when you need custom code, complex event processing, or lower latency.
  • Azure Container Apps — container-based hosting for long-running services. Functions on Flex Consumption beats it on cost for bursty, event-driven AI workloads that spend time idle.
  • Durable Functions — lives inside Functions and adds stateful, long-running orchestration. Use it for directed agentic workflows; use the serverless agents runtime for event-driven agents.

The underlying platform advantage

Across all four scenarios, the same hosting model applies: Flex Consumption. It offers fast, event-driven scaling, virtual network integration, and pay-as-you-go billing. For AI workloads, which tend to be bursty rather than continuous, this is a significant cost advantage over always-on hosting.

Managed identity, Application Insights integration, and azd-based deployment are consistent across all four patterns. That means your security posture, observability, and deployment pipeline do not have to change when you move from a simple timer trigger to hosting a remote MCP server.

Azure Functions AI integration: Choosing the right pattern

Here is a simple decision table:

I want to…Use…
Build event- or schedule-triggered agents with MCP toolsServerless agents runtime (preview)
Expose tools to AI clients via the industry-standard protocolMCP binding extension (GA)
Orchestrate predictable, multistep AI-driven processesDurable Functions
Build a RAG pipeline with fast, parallel data retrievalAzure Functions + Azure OpenAI binding
Fully managed agent hosting with enterprise SLAsAzure AI Foundry Agent Service

What comes next

The rest of this series goes deep on each pattern. The next post covers the two MCP server hosting options in detail binding extension versus self-hosted SDK servers, including where the current preview constraints matter in practice.

Up next: Hosting Remote MCP Servers in Azure Functions: GA vs. Preview Options

Build an AI Tech News Aggregator: Azure Functions & Claude

There’s a lot of noise on the internet. Reddit, Hacker News, tech blogs, keeping up with what actually matters in enterprise software is a full-time job. So I built a fully automated system that does it for me, runs in the cloud, is powered by AI, and was deployed end-to-end in less than two hours using Claude Code.

Here’s how.

What We Built (What Claude did mostly)

A C# Azure Function that runs every hour and:

  1. Fetches posts from configurable Reddit subreddits and Hacker News
  2. Filters for recency only posts from the last 7 days
  3. Deduplicates across runs never evaluates the same URL twice
  4. Applies an AI editorial filter Claude decides what’s genuinely newsworthy
  5. Writes curated results to Azure Blob Storage as timestamped JSON

The output is clean, structured JSON ready to feed into a newsletter, dashboard, or notification system.

The Architecture

The system has three layers: data collection, AI filtering, and persistence.

Reddit RSS feeds ──┐

                   ├─► Aggregator Function ─► Claude AI Filter ─► Blob Storage

HN Firebase API ───┘         │

                              └─► State Store (seen URLs)

Tech Stack

ConcernChoice
RuntimeAzure Functions v4, .NET 8 isolated worker
Reddit dataPublic Atom/RSS feed (r/{sub}/top.rss)
HN dataFirebase REST API
AI filteringAnthropic Claude (claude-opus-4-6) via raw HttpClient
StorageAzure Blob Storage
ScheduleNCRONTAB timer trigger

Interesting Engineering Decisions

Reddit: RSS over JSON API

The Reddit JSON API (/top.json) started returning 403s without authentication. Rather than deal with OAuth, we switched to Reddit’s public Atom/RSS feed (no credentials required) and parsed it with System.Xml.Linq in a handful of lines. Simple wins.

Claude as an Editorial Filter

Instead of writing brittle keyword heuristics to judge whether a post is “real tech news,” we hand that job to Claude with a carefully crafted system prompt based on Editorial Guidelines:

A post qualifies if it is relevant to enterprise software development AND meets at least one of the following: Change, Innovation, or Emergent Ideas, and is not a minor patch release, pure marketing, or clickbait.

Claude receives posts in batches of 25, returns a JSON array of qualifying indices, and we map those back to posts. If the API is unreachable, the batch passes through unfiltered as a deliberate fail-safe so the pipeline never breaks.

We used structured JSON output (output_config.format.type = “json_schema”) to guarantee a parseable response every time, no regex needed.

Deduplication Without a Database

To prevent re-evaluating the same URLs across hourly runs (and paying for unnecessary AI API calls), we persist a rolling state file — state/seen-urls.json — in Blob Storage. On each run:

  • Load seen URLs into a HashSet<string> for O(1) lookup
  • Filter new posts against it
  • After filtering, mark all new posts as seen (not just the ones that passed the AI filter — rejected posts shouldn’t be retried)
  • Prune entries older than 7 days to keep the file small

No database, no Redis, no infrastructure overhead. A blob file is enough.

The AI Filter in Practice

A typical hourly run might look like this:

Fetched 312 posts from the last 7 days.

Deduplication: 47 new / 265 already seen (skipped).

Running news quality filter on 47 new posts…

News filter: 11/25 posts passed.

News filter: 9/22 posts passed.

Filter complete: 20/47 posts kept.

20 posts saved to 2026/03/24/09-00-01.json

Out of 312 raw posts, 20 make it through. That’s the kind of signal-to-noise ratio that makes a curated feed actually worth reading.

Deployment

The whole thing deploys with two commands:

# Push app settings (API keys, schedule, etc.)

az functionapp config appsettings set \

  –name FuncNewsAggregation \

  –resource-group rg-news-aggregators \

  –settings @appsettings.json

# Publish the function

func azure functionapp publish FuncNewsAggregation –dotnet-isolated

Done. The function is live, running on Azure’s infrastructure, costing pennies per day.

What’s Next

A few natural extensions:

  • Email or Slack digest — trigger a Logic App when a new blob is written
  • Web frontend — serve the JSON blobs as a read-only news feed
  • Scoring — weight HN scores more heavily now that RSS drops Reddit scores
  • More sources — dev.to, lobste.rs, or custom RSS feeds are easy to add

Takeaways

The most interesting lesson here isn’t the code, it’s the division of labor. Deterministic logic handles the mechanical work: fetching, deduplicating, and scheduling. The judgment call “Is this actually news?”  goes to the model.

That separation keeps the system simple, cheap to run, and easy to adjust. Change the system prompt, and you change the editorial policy. No retraining, no feature engineering.

Two hours from idea to deployed function. That’s the pace at which you can build now.


All source code is C# targeting .NET 8. The function runs on an Azure Consumption plan and incurs roughly $0 in hourly costs well within the free tier.