Grounded AI: making an LLM draft a project charter from real data — without hallucinating A developer built a grounded charter generator for the Django-based project-management tool VanillaPM that drafts PMBOK-aligned project charters using only the user's actual input documents, such as the business case, benefits management plan, and agreements. The system caps each input document at 8,000 characters and the total grounding context at 40,000 characters, and its system prompt instructs the model to emit a short placeholder sentence naming missing inputs rather than fabricate content. The developer says giving the model an acceptable way to say it lacks information is the difference between a demo and a tool. "Add AI" is the easiest line item on any 2026 roadmap and the easiest way to ship something worse than nothing. Wire an LLM to a "Generate charter" button, feed it the project name, and it will happily produce a beautifully-worded project charter full of objectives, success criteria and stakeholders that do not exist . In a governed project-management tool, a confident invention is worse than a blank field — someone might believe it. I wanted the generate button anyway, because there's a real version of it. Here's how I built a charter generator that drafts genuinely useful prose from the user's actual data, and refuses to make things up — in Django, bring-your-own-key, no framework magic. Not autopopulate. I already had a deterministic autopopulate: copy the sponsor from the project record, the dates from the schedule, and so on. That's great for fields , but a charter is mostly prose — a scope statement, an objectives section, a rationale — synthesised from the pre-project inputs the business case, the benefits plan, the agreements . You can't str your way to a scope statement. Autopopulate structurally can't do it. Not a free-form LLM. The other extreme — "here's the project name, write me a charter" — is exactly the hallucination machine. The model has no idea what your business case says, so it invents a plausible one. The useful thing lives in between: grounded synthesis. Read the real input documents, and ask the model to compose — using only what's actually there. The whole trick is that the model's only source of truth is text you supply from the user's own documents — never its training data. In VanillaPM every document type declares its input documents its ITTO prerequisites — the things that must exist before it . A charter's inputs are the business case, the benefits management plan, the agreements. So "grounding" is concrete: go read those documents. DOC CHAR CAP = 8 000 per input document TOTAL CONTEXT CAP = 40 000 the whole grounding context def input documents project, doc type : """ type, title, text for each declared prerequisite that exists and has readable text — each capped so one long doc can't dominate the context.""" out = for prereq in prerequisite types project, doc type : from the dependency graph doc = latest of type project, prereq text = document text doc walk the stored rich-text → plain text if text: out.append prereq, doc.title, text :DOC CHAR CAP return out Two boring-but-important details: document text walks the document's stored rich-text tree and pulls the prose the human actually wrote — not a JSON dump of every model. The grounding context is then just: the deterministic structured facts reused from autopopulate + these input-document excerpts, under a big honest header. This is the part people skip. The model will fabricate unless you make refusing the easier path. Three rules, and the second one is the whole ballgame: SYSTEM PROMPT = "You draft sections of a PMBOK-aligned project document.\n" "1. Ground every section ONLY in the provided project data and input " " documents below. They are your only source of truth.\n" "2. If the data a section needs is missing, write ONE short professional " " sentence stating what input is still needed — do NOT fabricate.\n" "3. Return JSON only: " '{"sections": {"key":"