Academy › Advanced Marketing Automation with Claude › Lesson 5
Your content as a source: grounded drafts
Make drafts cite your own material so claims have a source and brand facts stay correct. · 9 min read
Models fill gaps with plausible guesses. In marketing, a plausible guess is a wrong price, a feature you do not have, or a testimonial nobody gave. The fix is to hand the model your facts and require it to use only them.
What you will build
A tiny “source pack” and a prompt that forces every claim to point at a chunk id.
Figure 1. If a claim has no source chunk, it does not ship.
Step 1: Write a source pack
Create a folder of short, plain-text notes. Each note is one fact family, with an id you control.
sources/
pricing.txt # Plans and what each includes (dated)
features.txt # What the product does and does not do
cases.txt # Two real customer outcomes, with permission noted
policy.txt # Refunds, support hours, data handling
The file name is the source id. Put a date at the top of each file. Stale facts are the most common cause of confident, wrong copy.
Step 2: Select, do not dump
Sending everything is costly and noisy. Start with the simplest selection that works: match the words of the task against your notes.
# sources.py
"""Load a folder of dated notes, pick the relevant ones, and check that a draft cites them."""
import pathlib
import re
def load_sources(folder: str = "sources") -> dict:
"""File name (without .txt) is the source id."""
return {p.stem: p.read_text(encoding="utf-8") for p in pathlib.Path(folder).glob("*.txt")}
def select(sources: dict, task: str, limit: int = 3) -> dict:
"""Keyword match: rank notes by how many task words (4+ letters) they contain."""
words = set(re.findall(r"[a-z]{4,}", task.lower()))
scored = []
for sid, text in sources.items():
low = text.lower()
scored.append((sum(1 for w in words if w in low), sid))
scored.sort(key=lambda t: (-t[0], t[1]))
return {sid: sources[sid] for hits, sid in scored[:limit] if hits > 0}
def check_citations(draft: str, source_ids: set) -> list:
"""Return a list of problems. An empty list means the draft passed the mechanical checks."""
problems = []
for cited in set(re.findall(r"\[([a-z0-9_-]+)\]", draft)):
if cited not in source_ids:
problems.append(f"cites unknown source: {cited}")
if "[MISSING" in draft:
problems.append("draft reports missing facts")
for sentence in re.split(r"(?<=[.!?])\s+", draft):
if re.search(r"\d|\$|%", sentence) and not re.search(r"\[[a-z0-9_-]+\]", sentence):
problems.append(f"number without a source: {sentence[:60]}")
return problems
This is intentionally plain keyword matching. Many small marketing teams never need more. Reach for embeddings and a vector store only when you have hundreds of documents and simple matching clearly misses relevant ones.
Step 3: Require citations
Write the email using ONLY the sources below. After every sentence that states a
fact (price, feature, policy, result), add the source id in square brackets, for
example [pricing]. If the sources do not contain a fact you need, write
"[MISSING: describe the fact]" instead of inventing it.
Do not mention anything that is not in the sources.
SOURCES
[pricing]
...
[features]
...
TASK
Write a 100-word reply to a lead asking whether the Starter plan includes API access.
Step 4: Check the citations in code
A model can still cite wrongly, so verify mechanically before a human reads it. The checker lives in the same file as the selector; this is the part that runs on every draft.
Test it
def test_flags_unknown_source_and_unsourced_numbers(self):
problems = check_citations("The Pro plan costs $49 [pricing]. It has 5 seats [teams].", {"pricing"})
self.assertTrue(any("unknown source: teams" in p for p in problems))
problems = check_citations("It costs $19 per month.", {"pricing"})
self.assertTrue(any("number without a source" in p for p in problems))
A draft with any problem goes back for a rewrite or to a human, never straight to the queue.
Worked example
A SaaS team asked Claude for a pricing reply with no sources and got a plan name that did not exist. With a dated pricing.txt and the citation rule, the same request returned a reply that cited [pricing] for each figure, and the one fact they had not written down (API rate limits) came back as [MISSING: API rate limits], which told them exactly which note to add.
Common mistakes
- Giving the model a 40-page PDF and hoping.
- Letting sources go stale. Add a “last checked” date and a calendar reminder.
- Treating a citation as proof. Verify the cited chunk actually says it.
Checklist
- Source pack with dates and ids
- Prompt requires citations and allows “MISSING”
- Code checks citations and numbers before human review
- A review date set for each source file