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.

Your content split into chunks, relevant ones selected, then a draft that cites chunk ids 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