Creating a compelling newsletter consistently can be a challenging, time-consuming task. In this tutorial, we’ll walk through building a custom AI-powered newsletter generator – a tool that helps you draft, curate, and send newsletters with minimal manual effort. We will cover the full system architecture, from data ingestion to AI content generation to email campaign delivery. The solution leverages GPT-trainer as the AI orchestration backend, taking advantage of its multi-agent Retrieval-Augmented Generation (RAG) workflow and support for various Large Language Models (LLMs) including OpenAI GPT models, Anthropic Claude, Google Gemini, and the open source DeepSeek.
What we'll build: A system where you can upload reference documents, configure an AI to draft newsletter content based on those references and your guidance, iteratively refine the drafts (maintaining version history), and finally schedule and send the newsletter via an email service (e.g. SendGrid). Throughout the process, we’ll demonstrate how GPT-trainer streamlines complex AI tasks – you don’t need to reinvent RAG pipelines or manage vector databases yourself. Instead, GPT-trainer takes care of that for you under the hood.
By the end of this tutorial, you will have a clear blueprint for implementing a full-stack AI newsletter generator. We’ll provide code snippets (Python/JavaScript) for key components – such as using GPT-trainer’s API for content generation, handling file uploads, formatting Markdown, and integrating with SendGrid’s email API – along with recommendations on tech stack choices for each part of the system. Let’s dive in!
Before we get into the details, it’s important to understand the overall architecture of our newsletter generator. The system consists of several components working together:
{{{name}}}
or {{{entity}}}
in the content that get replaced per recipient). We’ll also support using SendGrid Dynamic Templates: you can design an email template in SendGrid (or use none for a simple default), and the system will send the newsletter content either standalone or injected into that template by specifying its template ID. Once sent, we rely on SendGrid’s dashboard for delivery and engagement statistics, rather than building a custom analytics UI.Below, we’ll go through each of these components in detail, with guidance on implementation and example code.
For our AI to generate insightful newsletters, it needs a knowledge base of reference material. This could include past newsletter editions, company blog posts, research papers, marketing materials – any documents that contain information you want the AI to draw from. Our system provides two main ways to add content to the knowledge base: direct file upload and Google Drive import.
On the backend, we’ll use GPT-trainer’s API directly to handle file uploads. Each uploaded document will be stored and indexed for retrieval. GPT-trainer offers a convenient API for uploading files as “data sources” attached to an AI chatbot agent. Under the hood, GPT-trainer will parse the file (PDF, DOCX, TXT, etc.), chunk it if necessary, and add it to a vector index so that its content can be retrieved during generation.
For example, using GPT-trainer’s API, you can upload a file with an HTTP POST request. Here’s a snippet in Python demonstrating how to upload a file via GPT-trainer’s API:
1import requests 2 3CHATBOT_UUID = "<your_chatbot_id>" # ID of the GPT-trainer chatbot/agent 4API_KEY = "<your_api_key>" 5file_path = "newsletter_research.pdf" 6 7url = f"https://app.gpt-trainer.com/api/v1/chatbot/{CHATBOT_UUID}/data-source/upload" 8headers = {"Authorization": f"Bearer {API_KEY}"} 9 10# Open the file in binary mode and send in multipart form 11files = {"file": open(file_path, "rb")} 12data = {"meta_json": '{"reference_source_link": ""}'} 13response = requests.post(url, headers=headers, files=files, data=data) 14print(response.json())
In this request, we include an optional meta_json
with a reference_source_link
(which could be a URL pointing to the original source of the file, if applicable). GPT-trainer will respond with metadata about the uploaded file (e.g. title, size, token count, and a unique source ID). After uploading, the file’s content is now part of the chatbot’s knowledge library. By default, the AI agent will have access to all sources in its knowledge library when answering queries (you can configure an agent to use_all_sources = true
to ensure this). This means our newsletter drafting agent will consider all uploaded documents as potential reference material, unless we explicitly restrict the set (we’ll discuss restricting sources shortly).
On the frontend, you might implement a simple drag-and-drop UI or a file picker for users to upload documents. The uploaded files can be listed in a “Library” section, showing filename, upload date, etc. Each entry can also have a delete option. Deleting a document would call GPT-trainer’s delete source API (or remove it from your database/storage and then call delete). For instance, GPT-trainer provides a DELETE /api/v1/chatbot/{uuid}/data-source/{source_id}
endpoint to remove a source, or a bulk delete endpoint. Always ensure that when documents are deleted, any indexes or caches are updated (GPT-trainer’s retrain sources
endpoint can re-index remaining data if needed).
Tech Stack Tips: You can use a backend framework like FastAPI (Python) implement the file upload endpoint and relay files to GPT-trainer’s API. For storing files, if you need to keep a copy, you might use cloud storage (AWS S3, Google Cloud Storage) or a database if files are small (though GPT-trainer stores the content internally once uploaded). However, since GPT-trainer handles the heavy lifting of parsing and indexing, you may not need to store the file contents yourself at all – storing just the metadata (title, source link, GPT-trainer source ID) in your database could suffice for reference.
To make it easy to bring in existing content, you can integrate a Google Drive importer. This would allow a user to authenticate with Google and select files from their Drive to import into the newsletter tool. Implementation-wise, you’d use the Google Drive API to list and download files. Once downloaded, you can forward the file to the same upload process described above.
A typical flow for Google Drive import:
files.get
with alt=media
to download the file content. Then treat it as an upload – i.e., send it to GPT-trainer’s upload API (or your own upload logic). You might show a progress bar if files are large.We won’t dive into full code for Google API here, but Google’s documentation provides examples on listing and downloading files. The key point is that once you have the file content, you feed it into the same pipeline so it becomes part of the knowledge base.
If you have many documents, it can be useful to organize them by tags or categories. GPT-trainer’s API supports tagging data sources. You could allow users to tag uploads (e.g. “industry report”, “Q1 2025 newsletter”, etc.) and then configure your AI agent to use only certain tags for certain newsletter drafts. By default, our agent will use all sources. For simplicity, we’ll assume all sources are available for RAG, unless a user explicitly filters which ones to use for a given draft.
With our data in place, let’s move on to configuring the AI that will generate the newsletter content.
In this phase, the user will input some guidance for the newsletter, and the AI will produce a draft. We recommend several configurable aspects here:
Let’s break down these steps.
If your team sends newsletters regularly, you might want to reuse certain settings. For example, you might always use the same tone and audience, or you might have different templates for an engineering newsletter vs. a marketing newsletter. Our tool can allow saving these preferences as templates.
A configuration template may include fields like:
You can let users set up these templates via a form in the UI and save them to a database. Each template might be a JSON blob or database row. For example:
1{ 2 "name": "Engineering Monthly Template", 3 "model": "gpt-4-16k", 4 "tone": "Knowledgeable and friendly", 5 "audience": "Software engineers at ACME Corp", 6 "style_notes": "Use a conversational tone with tech jargon where appropriate.", 7 "citation_style": "inline", 8 "default_sources": ["all"], 9 "created_by": "user123" 10}
Loading a template would pre-fill the generation form with these values, which the user can tweak before generating the draft.
By default, GPT-trainer agents will consider all uploaded data sources when responding to a query (our newsletter prompt). However, sometimes a newsletter might only need a subset of the data. For instance, if you have documents from multiple projects but this newsletter is only about “Project X”, you might want to limit the references to Project X’s documents.
To support this, your UI can present a list of available documents (from the Data Management phase) with checkboxes or tags, letting the user select which ones to include as context. If no selection is made, assume all documents are fair game.
The user inputs that guide the AI draft are critical. Our UI will have fields for:
When generating the newsletter, we will combine all these into a single prompt instructing the AI. A prompt template could be something like:
“You are an AI Newsletter Writer. Draft a newsletter on the topic of {Topic}, aimed at {Audience}. The tone should be {Tone}. Include the following key points in the newsletter: {Key Ideas list}. Organize the content with clear headings and subheadings, and use bullet points or numbered lists where appropriate to improve readability. If you reference facts or data from the provided sources, cite them in the text (e.g., as inline citations like [1] or [Source]) so that the source list can be included. Provide the output in valid Markdown format.”
We will feed this prompt to our GPT-trainer agent, along with the agent’s knowledge base enabled. Because our agent is connected to the uploaded data, it can pull in relevant content. For instance, if one key point is “upcoming conference participation”, and one of the uploaded documents is a press release about that conference, the AI can quote or summarize from that press release.
At runtime, you will replace {Topic}
, {Audience}
, {Tone}
, {Key Ideas list}
with the user’s input. This assembly happens in your backend service or client code.
Once the prompt is ready, it’s time to call GPT-trainer to get the AI-generated content. With GPT-trainer, the typical pattern to get a response is:
Here’s an example using Python requests
to obtain a draft from GPT-trainer:
1import requests 2import json 3 4API_KEY = "<your_api_key>" 5CHATBOT_UUID = "<your_chatbot_id>" # The chatbot configured for newsletter drafting 6 7# 1. Create a new chat session 8session_url = f"https://app.gpt-trainer.com/api/v1/chatbot/{CHATBOT_UUID}/session/create" 9headers = {"Authorization": f"Bearer {API_KEY}"} 10session_resp = requests.post(session_url, headers=headers) 11session_id = session_resp.json().get("uuid") 12 13# 2. Send the newsletter prompt to the session 14message_url = f"https://app.gpt-trainer.com/api/v1/session/{session_id}/message/stream" 15prompt = { 16 "query": assembled_prompt_text # The prompt text we crafted with Topic, Tone, etc. 17} 18response = requests.post(message_url, headers=headers, json=prompt, stream=True) 19 20draft_markdown = "" 21for chunk in response.iter_content(chunk_size=None): 22 if chunk: 23 draft_markdown += chunk.decode('utf-8') # accumulate streamed content
In this snippet, we use the streaming endpoint (message/stream
) to get the response. We accumulate the chunks to build the full Markdown output. If you prefer not to handle streaming, GPT-trainer may also support a non-streaming message endpoint (often it might just buffer internally, but streaming is nice for responsiveness if you show a loading animation with partial text).
After this call, draft_markdown
will contain the newsletter content in Markdown format. For example, it might look like:
1# ACME Corp Tech Newsletter – Q3 2025 2 3## 1. Product Launch in Europe 4 5Our new XYZ product line is launching in Europe next month, following its success in the US. Early feedback from beta users has been **very positive**, with demand projections exceeding initial targets【reference1†】. 6 7... (more content) ... 8 9## 2. Recent Funding Round 10 11ACME Corp secured a $50M Series B funding round led by BigVC Capital【reference2†】. This injection of capital will accelerate our R&D and hiring, especially in the AI and ML teams. 12 13... (more content) ... 14 15_Sources:_ 16【reference1】 ACME Corp Internal Beta Testing Report – Aug 2025 17【reference2】 TechCrunch article “ACME Corp raises $50M...” (2025)
The AI has followed instructions to create headings, used an ordered list for main points, and added citations in a placeholder format (e.g. 【reference1†】). The exact format of citations can be controlled by how you prompt. If you want numeric inline citations like “[1]”, you can ask for that style. The example shows a possible approach where the AI labeled sources as reference1, reference2, and listed them at the end.
Tip: Achieving perfect citation formatting might require iterating on the prompt. GPT-trainer’s recent updates show improvements in including RAG context and sources in responses. You can instruct the agent explicitly: “for any fact you use from the sources, add an inline citation in the form [^1^] and include a 'Sources' section at the end listing each reference with a number.” The AI will then try to output properly formatted citations. Alternatively, you may capture metadata about which sources were retrieved and post-process the draft to append source links.
At this point, we have an AI-generated newsletter draft in Markdown. Next, we want to allow a human to review and refine it, which brings us to version management and manual curation.
No matter how good the AI is, human oversight is important for a polished newsletter. Our system will keep track of different versions of the newsletter content and make it easy to iterate.
We introduce the concept of a draft family to group related versions of a newsletter. Each time you click “Generate AI Draft”, you start a new family, and the initial AI output is the root version. For example, if you’re working on the October newsletter, you might generate an initial draft – that’s version 1.0 (root) in a new family “October 2025 Newsletter”. Suppose you then make some edits and save – that becomes version 1.1 (a human-edited branch). If you generate again from scratch (perhaps with different prompts) for the same edition, that would start a new family. A simple approach is: each distinct AI generation session = new family, and within a family, any manual saves are branches within that family.
Implementing this can be as simple as maintaining a table:
draft_family
table: id, name (or date), description.draft_version
table: id, family_id, version_number, content, created_by, created_at, parent_version_id (nullable).Whenever an AI draft is created, create a new family entry and a version entry (with parent = null since it’s root). For a manual edit save, create a new version with parent set to the root (or the version it was edited from). You could also allow branching off the latest human version – that’s up to how granular you want to track, but root vs edited is usually enough.
In the UI, present this as a list of drafts. For example:
This helps users keep track of iterations and ensures the AI output isn’t lost or overwritten accidentally.
To facilitate manual curation, our tool will include a text editor for the newsletter content. Since the content is in Markdown, an ideal editor would support Markdown syntax highlighting and possibly a preview pane. There are many open-source web components available for this:
<textarea>
with a live preview (using a library like marked.js to render HTML) can do the job for a minimal approach.When the user opens an AI draft, load the Markdown into the editor. They can make changes (fixing tone, updating figures, adding an intro or conclusion, etc.). When they click “Save Version”, you’ll take the edited Markdown and save it as a new version (in the database and perhaps also as a file if you want to keep file history). This new version is linked to the original family as described.
To avoid confusion, you might lock the AI draft from direct editing – i.e. always create a new version for edits from the root version, so the original AI output remains intact for reference. You can visually indicate which versions were AI-generated vs human-edited.
Since we want to ultimately email this content, we need it in HTML format (email clients don’t render Markdown). So as part of the save or send process, we convert Markdown to HTML.
Converting Markdown to HTML can be done with a variety of libraries:
markdown
library (import markdown; html = markdown.markdown(markdown_text)
).On the backend, it’s often safer to use a library to avoid any XSS issues (though if your Markdown is internal, XSS is less a concern unless the editors themselves inject scripts). Many Markdown libraries allow whitelisting or removing dangerous HTML.
For example, using Python’s markdown library:
1import markdown 2md_text = open("draft_oct2025_v1_2.md").read() 3html_content = markdown.markdown(md_text, extensions=["tables", "fenced_code"])
This will turn our newsletter Markdown into an HTML string, which can then be placed into an email template or sent as the email body.
If you need to inject this into an existing HTML template (like a SendGrid saved template), you might instead separate the concerns: you can store the Markdown as the source of truth, but when sending via SendGrid with a template, you might just send the content as a substitution value (more on this in the next section).
At this stage, the assumption is we have a final Markdown that has been reviewed and approved. We convert it to HTML (we might also keep the Markdown around for record-keeping or future reference). Now we’re ready to send out the newsletter.
With a final draft ready, the last step is to deliver it to subscribers. Our system’s campaign management features will cover:
Our UI should provide a Campaign interface where the user can:
On submission, the backend will compile the email data and call the SendGrid API. It should also create a record of the campaign in a campaign
table (with fields like: draft_version_id, template_id, send_time, status, etc.), so you have a log of what was sent and when. This is useful for audit and for listing past campaigns in the UI.
You may have multiple email lists (for example, Customers, Partners, Internal, etc.) or you may have one master list with attributes to filter by. For simplicity, let’s say we maintain a list of subscribers in a database table with fields like: email, name, company, tags (which could be a JSON or a separate table relating subscriber IDs to tag names). Tags might include things like “customer”, “beta-user”, “region_europe”, etc., allowing segmentation.
When preparing a campaign, the user should be able to select which group of recipients to send to. Once the recipients are determined, we also want to personalize the content. Common personalization includes addressing the person by name, or referencing their company or other tokens in the email. We might have placed placeholders in the newsletter content like {{{name}}}
or {{{entity}}}
. This triple-brace style is reminiscent of Handlebars (which SendGrid uses for dynamic templates).
Using SendGrid Dynamic Templates: The cleanest way to handle personalization is to use SendGrid’s template functionality. You can create a dynamic template in the SendGrid dashboard with placeholders for variables. For example, your template HTML might have Hello {{{name}}},
somewhere. When sending via the API, you provide a dynamic_template_data
JSON with values for each placeholder. SendGrid will then generate the final email for each recipient, merging in their specific data.
If not using templates, you can send raw HTML content via the API directly for each email, but that’s less efficient if you have many recipients (it’s better to use the bulk send with a single template).
For our design:
draft_html
we generated should be used as the email body as-is (perhaps with a default styling).draft_html.replace("{{{name}}}", recipient_name)
and so on, then send individually. But that’s not great for large lists or maintainability. We’ll opt for using the template approach for a real system.SendGrid provides APIs (and client libraries in many languages) to send emails. We’ll illustrate using Python, showing how to send via SendGrid’s Python library, including template usage and scheduling:
1import os 2from sendgrid import SendGridAPIClient 3from sendgrid.helpers.mail import Mail, From, To, Personalization 4from datetime import datetime, timezone 5 6# SendGrid API key and template ID 7SENDGRID_API_KEY = os.getenv("SENDGRID_API_KEY") 8TEMPLATE_ID = os.getenv("SENDGRID_TEMPLATE_ID") # Your SendGrid dynamic template ID 9 10# HTML content of your finalized newsletter 11newsletter_html = final_draft_html 12 13# List of recipients with personalization data 14recipients = [ 15 {"email": "alice@example.com", "name": "Alice", "entity": "ACME Corp"}, 16 {"email": "bob@example.com", "name": "Bob", "entity": "ACME Corp"}, 17 # Add more recipients as needed 18] 19 20# Initialize SendGrid client 21sg_client = SendGridAPIClient(SENDGRID_API_KEY) 22 23# Create a Mail object with sender details and template 24message = Mail() 25message.from_email = From(email='news@yourdomain.com', name='Your Newsletter') 26message.template_id = TEMPLATE_ID 27 28# Add personalizations for each recipient 29for recipient in recipients: 30 personalization = Personalization() 31 personalization.add_to(To(email=recipient["email"], name=recipient["name"])) 32 personalization.dynamic_template_data = { 33 "name": recipient["name"], 34 "entity": recipient["entity"], 35 "body": newsletter_html 36 } 37 message.add_personalization(personalization) 38 39# Optional: Schedule email for future sending (e.g., 2025-11-01 10:00 UTC); note that it may be better to manage scheduling within your integration instead, as editing the time later would be more complicated if we push it off to Sendgrid now 40send_time = datetime(2025, 11, 1, 10, 0, 0, tzinfo=timezone.utc) 41message.send_at = int(send_time.timestamp()) 42 43# Send the email 44try: 45 response = sg_client.send(message) 46 print(f"Newsletter scheduled successfully! Status Code: {response.status_code}") 47except Exception as e: 48 print(f"An error occurred: {e}")
A few things to note in this code:
We use a SendGrid dynamic template. The template would contain placeholders like {{{name}}}
, {{{entity}}}
, and {{{body}}}
(the triple braces {{{ }}}
in Handlebars mean “don’t escape HTML”, which we want for injecting the already-formatted newsletter content). Our dynamic_template_data
provides the actual values for each recipient.
We set up multiple personalizations – one for each recipient – each with their own data. This allows SendGrid to send a batch in one API call (rather than one call per email).
If TEMPLATE_ID
is not provided (meaning we decided to send raw), we would instead do something like:
1message = Mail( 2 from_email='news@yourdomain.com', 3 to_emails=[recipient["email"] for recipient in recipients], 4 subject="ACME Corp Monthly Newsletter – October 2025", 5 html_content=newsletter_html 6)
and send that. But then personalization would require us to manually embed each name in the newsletter_html
string, which is messy. Using templates is cleaner for personalization at scale.
If you prefer a different email service (like Mailchimp, SES, etc.), the concept is similar: you either send via API with template and substitutions or you craft the MIME message yourself. We chose SendGrid for its ease and the fact it’s mentioned in our requirements.
Once the emails are sent, SendGrid will handle delivery to each recipient. To see how the campaign performed (opens, clicks, bounces, etc.), you typically use SendGrid’s web dashboard or their Event Webhook for advanced tracking. Given that our tool is aimed at content generation and sending, we can direct users to the SendGrid dashboard for analytics. However, you might integrate basic stats via SendGrid’s APIs:
Covering analytics integration is beyond our scope here, so a simple solution is to provide a link: “View detailed analytics on SendGrid” which takes the user to the SendGrid campaign or stats page. This leverages SendGrid’s robust analytics UI instead of duplicating it.
By following this end-to-end guide, you can build a powerful AI-assisted newsletter generator that significantly streamlines the content creation process for newsletter email campaigns. We covered how to manage a repository of knowledge (documents) for the AI, how to configure GPT-trainer agents to generate drafts using retrieval augmented generation, and how to allow humans to refine those drafts with version control in place. We also integrated with an email delivery system to handle the final step of sending the content out as a polished newsletter.
A few key takeaways and advantages of using GPT-trainer in this architecture:
For the tech stack, you have flexibility. For instance, a modern implementation could use React (with a Markdown editor component) for the frontend, a Python/FastAPI backend for handling API calls and database ops, a PostgreSQL or MongoDB for storing metadata (templates, drafts, subscribers), and rely on GPT-trainer’s API for all AI tasks and SendGrid for email delivery. Each component we chose can be swapped (you could use another email API, etc.), but the architecture would remain largely the same.
We hope this tutorial has illuminated how to put together a custom newsletter generator that saves you time and effort.