Microsoft Agent Framework: Implementing an AI Email Marketing Agent
In an earlier post in this series, we built an AI agent that researches the latest AI news and generates a weekly digest newsletter as a local markdown file.
I’ve been using that agent personally for several weeks now to stay on top of what’s happening across Microsoft, Google, Anthropic, and OpenAI.
It’s become part of my weekly routine.
I had an idea. What if my blog subscribers could get this digest too?
In this post, we’ll take that existing agent and extend it so that instead of just saving a file to disk, it automatically creates and sends an email broadcast via AWeber.
AWeber is a marketing tool. I usedit to capture email address and communicate with blog subscribers.
This post shows how to :
- integrate AWeber’s REST API using OAuth 2.0
- convert our markdown newsletter into email-safe HTML,
- give the agent the ability to schedule broadcasts for delivery.
Lets dig in.
~
What We’re Building On
If you haven’t read the previous post, here’s a quick recap. We built an AI agent using the Microsoft Agent Framework with OpenAI background responses that:
- Searches the
Microsoft Foundryblog for AI platform updates - Fetches `Microsoft Agent Framework` commits and releases from
GitHub - Searches RSS feeds (TechCrunch, Wired, Azure Blog) for company-specific AI news
- Compiles everything into a structured markdown newsletter with sections, predictions, and source links
The agent uses function tools to gather data and a GenerateNewsletter tool to save the result.
The background responses pattern lets us poll for progress while the agent works through multiple API calls.
All of that stays the same. What changes is the final step.
~
The Plan
Instead of just writing a markdown file, we want the agent to:
- Save the markdown locally (keeping the existing behaviour)
- Convert the markdown to email-safe HTML
- Create a broadcast draft in AWeber via their API
- Optionally schedule the broadcast for delivery
We also want to add a new data source: the latest blog post from jamiemaguire.net, so subscribers get a “From the Editor” section in each digest.
~
Project Structure
Here’s what the new project looks like:
Agent-Framework-8-AWeber-Newsletter/
├── Agent-Framework-8-AWeber-Newsletter.csproj
├── Program.cs
├── Agents/
│ └── AINewsServiceAgent.cs
├── Models/
│ ├── NewsItemModel.cs
│ ├── AWeberTokens.cs
│ └── AWeberApiModels.cs
├── Services/
│ └── AWeberClient.cs
└── aweber_tokens.json
The key additions compared to the previous project are the Services/AWeberClient.cs for the AWeber integration, the token/API models, and the Markdig NuGet package for markdown-to-HTML conversion.
~
Setting Up AWeber OAuth 2.0
Before we can send emails, we need to authenticate with AWeber. Their API uses OAuth 2.0 with token refresh.
One-time setup:
- Register your app at https://labs.aweber.com/
- Set the OAuth Redirect URL to
urn:ietf:wg:oauth:2.0:oob(this is the out-of-band flow for desktop/CLI apps – instead of redirecting to a URL, AWeber displays the auth code on screen for you to copy) - Authorise the app and exchange the code for tokens
The aweber_tokens.json file stores the credentials:
{
"client_id": "your_client_id",
"client_secret": "your_client_secret",
"access_token": "your_access_token",
"refresh_token": "your_refresh_token",
"account_id": "",
"list_id": ""
}
The account_id and list_id fields are left empty – the client discovers them automatically on first run and saves them back to the file.
~
The AWeber Client
The AWeberClient class handles all the AWeber API interaction. It manages token persistence, auto-refresh, account discovery, broadcast creation, and scheduling.
Here’s the token refresh flow:
private async Task RefreshAccessTokenAsync()
{
var credentials = Convert.ToBase64String(
Encoding.UTF8.GetBytes($"{_tokens.ClientId}:{_tokens.ClientSecret}"));
var request = new HttpRequestMessage(HttpMethod.Post, TokenUrl);
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", credentials);
request.Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "refresh_token",
["refresh_token"] = _tokens.RefreshToken
});
var response = await _httpClient.SendAsync(request);
var responseBody = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
throw new HttpRequestException(
$"Failed to refresh AWeber token: {response.StatusCode} - {responseBody}");
using var doc = JsonDocument.Parse(responseBody);
var root = doc.RootElement;
_tokens.AccessToken = root.GetProperty("access_token").GetString() ?? "";
_tokens.RefreshToken = root.GetProperty("refresh_token").GetString() ?? "";
SaveTokens();
}
AWeber access tokens expire every 2 hours, so every authenticated request goes through a wrapper that catches 401 Unauthorized, refreshes the token, and retries:
private async Task<HttpResponseMessage> SendAuthenticatedRequestAsync(
HttpMethod method, string url, HttpContent? content = null)
{
var response = await SendRequestAsync(method, url, content);
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
await RefreshAccessTokenAsync();
response = await SendRequestAsync(method, url, content);
}
return response;
}
Creating a broadcast is a POST to the AWeber API with the email subject, HTML body, and plain text body:
public async Task<AWeberBroadcastResponse> CreateBroadcastAsync(
string subject, string bodyHtml, string bodyText)
{
await EnsureAccountAndListAsync();
var url = $"{BaseUrl}/accounts/{_tokens.AccountId}/lists/{_tokens.ListId}/broadcasts";
var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["subject"] = subject,
["body_html"] = bodyHtml,
["body_text"] = bodyText
});
var response = await SendAuthenticatedRequestAsync(HttpMethod.Post, url, content);
var responseBody = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
throw new HttpRequestException(
$"Failed to create AWeber broadcast: {response.StatusCode} - {responseBody}");
return JsonSerializer.Deserialize<AWeberBroadcastResponse>(responseBody)
?? throw new InvalidOperationException($"Failed to parse broadcast response");
}
~
Replacing GenerateNewsletter with CreateAndSendNewsletter

This is where the previous project’s GenerateNewsletter function tool gets replaced. The new CreateAndSendNewsletter does everything the old one did, plus the AWeber integration.
[Description("Creates an email newsletter from the gathered news, saves it locally as markdown, " +
"and sends it as an AWeber broadcast. Call this after gathering news from all companies.")]
public static async Task<string> CreateAndSendNewsletter(
[Description("The newsletter/email subject line")] string subject,
[Description("The full markdown content of the newsletter")] string markdownContent,
[Description("Minutes from now to schedule sending. Use 0 to create as draft only.")] int scheduleMinutesFromNow = 0)
The flow inside the method is:
- Save markdown locally – same as before, preserving the existing behaviour
- Convert markdown to HTML using Markdig
- Wrap in an email-safe HTML template with inline styles (email clients don’t support external CSS)
- Create the AWeber broadcast via the API
- Optionally schedule the broadcast for delivery
The markdown-to-HTML conversion uses Markdig’s advanced extensions pipeline:
var pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
var innerHtml = Markdown.ToHtml(fullMarkdown, pipeline);
var bodyHtml = WrapInEmailTemplate(innerHtml, subject);
The HTML template uses table-based layout with inline styles – the standard approach for email HTML that renders consistently across Gmail, Outlook, Apple Mail, and other clients.
If the AWeber call fails, the method still returns successfully (the markdown file was already saved) and includes the error message so the agent can report it.
~
Adding a Personal Touch: The Blog Feed

I wanted each newsletter to include a “From the Editor” section featuring my latest blog post. This meant adding a new function tool: SearchJamieMaguireBlog.
A problem was my WordPress site’s security plugin blocks programmatic requests from .NET’s HttpClient – even when setting with browser agent headers.
The solution was to use the WordPress REST API (/wp-json/wp/v2/posts) and fetch it via Windows’ built-in curl.exe, which uses a different TLS stack (WinHTTP) that doesn’t get flagged:
[Description("Fetches the most recent blog post from jamiemaguire.net.")]
public static async Task<List<NewsItemModel>> SearchJamieMaguireBlog()
{
var process = new System.Diagnostics.Process
{
StartInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = "curl.exe",
Arguments = $"-s -L \"{apiUrl}\"",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}
};
process.Start();
var json = await process.StandardOutput.ReadToEndAsync();
await process.WaitForExitAsync();
// Parse the WordPress REST API JSON response...
}
This is a pragmatic workaround. Not ideal but in the interests of time, was a quick way to get around this.
~
Updated Agent Instructions
The agent’s workflow in Program.cs now includes the new data source and the AWeber step:
AIAgent agent = responseClient.CreateAIAgent(
name: "AI News Digest Agent",
instructions: """
You are an AI news research agent that creates weekly digest newsletters
and sends them via AWeber.
Workflow:
1. Call SearchFoundryBlog() to get Microsoft AI Foundry updates
2. Call SearchAgentFrameworkUpdates() to get Microsoft Agent Framework code updates
3. Call SearchCompanyNews for each company: Microsoft, Google, Anthropic, OpenAI
4. Call SearchJamieMaguireBlog() to get the latest blog post from jamiemaguire.net
5. Call CreateAndSendNewsletter to send the newsletter via AWeber
...
""",
tools: [
AIFunctionFactory.Create(AINewsServiceAgent.SearchFoundryBlog),
AIFunctionFactory.Create(AINewsServiceAgent.SearchAgentFrameworkUpdates),
AIFunctionFactory.Create(AINewsServiceAgent.SearchCompanyNews),
AIFunctionFactory.Create(AINewsServiceAgent.SearchJamieMaguireBlog),
AIFunctionFactory.Create(AINewsServiceAgent.CreateAndSendNewsletter)
]);
The newsletter includes a few sections:
- overview
- six news source sections
- from the editor
- what to watch trends
- predictions for the next 90 days with confidence scores
Useful for skim reading. Each section contains link that take you directly to the detail.
~
The Function Tools
Here’s a summary of the tools available to the agent:
| Tool | Description |
|---|---|
SearchFoundryBlog |
Scrapes the Microsoft Foundry blog for the latest AI platform posts |
SearchAgentFrameworkUpdates |
Fetches recent commits and releases from the Agent Framework GitHub repo |
SearchCompanyNews |
Searches RSS feeds for AI news about a specific company |
SearchJamieMaguireBlog |
Fetches the latest blog post from jamiemaguire.net via the WordPress REST API |
CreateAndSendNewsletter |
Saves markdown locally, converts to HTML, creates AWeber broadcast, optionally schedules delivery |
~
From Personal Tool to Subscriber Newsletter

I’d been running the previous version of this agent weekly as a personal productivity tool.
It saved me time.
Instead of manually checking multiple blogs, RSS feeds, and GitHub repos, I’d just type Generate this week's digest and get a comprehensive overview in about two minutes.
The jump to AWeber integration means this digest is now available to subscribers of my blog. The agent does the same research it always did, but now the output goes directly into an email broadcast that reaches anyone who’s signed up.
What started as a personal convenience has become a value-add for the community.
The scheduleMinutesFromNow parameter also means I can review the draft in AWeber’s dashboard before it goes out, or schedule it for a specific delivery window.
Setting it to 0 creates a draft only, which is what I use – I like to review the content before it reaches subscribers.
~
Demo
We can see the revised agent with AWeber integration in action here. In this demo, we launch the agent, then verify the draft newsletter has been scheduled for delivery in the AWeber dashboard.
Summary
In this post, we extended our AI news digest agent to:
- Integrate with AWeber’s REST API using OAuth 2.0 with automatic token refresh
- Convert markdown to email-safe HTML using Markdig, wrapped in a table-based template with inline styles
- Create broadcast drafts that appear in the AWeber dashboard ready for review or scheduling
- Add a personal blog feed as a new data source for the “From the Editor” newsletter section
- Handle the full email pipeline – from AI-powered research to formatted email broadcast – in a single agent run
The key architectural decision was replacing the GenerateNewsletter function tool with CreateAndSendNewsletter while keeping the same agent workflow.
The agent doesn’t know or care that its output is going to an email platform. It just calls a function tool with a subject and markdown content. The tool handles the conversion and delivery.
~
Enjoy what you’ve read, have questions about this content, or would like to see another topic?
Drop me a note below.
You can schedule a call using my Calendly link to discuss consulting and development services.