System prompts are generated documents, not static strings

A system prompt written once as a hardcoded string quietly goes stale the moment anything it references changes. Real systems rebuild it per request, injecting whatever's actually true right now.

Beginner

5 min read

The trap: a prompt that never mentions the current date

An early, easy-to-miss mistake: a system prompt with no notion of "today" at all. The model has no way to know the actual current date unless it's told — and left to its own devices, it can produce dates from its training data's era, or simply guess, neither of which is "now." A real, documented instance of this: expense-recording proposals that got confirmed successfully, but with dates from over a year in the past, because nothing in the prompt ever supplied the actual current date. The fix was mechanical once identified — prepend the real server date (in the correct local timezone) to the system prompt on every request.

buildSystemPrompt():
  const today = new Date().toLocaleDateString("en-US", { timeZone: "..." });
  return `Today's date is ${today}. ...rest of the prompt...`

The general principle: a system prompt is a function, not a constant

Wrong mental model:  SYSTEM_PROMPT = "You are a helpful assistant..."
                      (written once, reused unchanged forever)

Right mental model:   buildSystemPrompt(request) -> string
                       (rebuilt on every single request, assembling
                       whatever is actually true at that moment)

Anything genuinely true "right now" but not true forever belongs in this rebuilt-per-request category: the current date, feature flags currently enabled, and — critically, covered in this domain's authorization lesson — exactly which write actions the specific calling user is currently allowed to perform. A prompt is the model's entire picture of the world for that conversation; anything the model needs to reason correctly about has to actually be in it, not assumed.

Why this matters more for agents with tools than for a plain chatbot

A chatbot that only talks can get away with a vaguer prompt — a slightly wrong tone is a minor issue. An agent that can call tools, and especially one whose tools can write real data, compounds a stale-context problem into an actual data-correctness bug: the "hallucinated old date" example above didn't just produce a wrong sentence, it produced a wrong row in a database, confirmed by a real user who had no reason to double-check a date field they weren't focused on.

Permissions belong in the prompt too, not just tool availability

It's tempting to think "the model only has the tools it has — that's the permission boundary." In practice, a stronger and more honest design also tells the model, in plain language, what the calling user is and isn't allowed to do, computed server-side and included as part of the assembled prompt — not just leaving the model to discover a restriction only when it tries to call a tool and gets rejected mid-conversation. This produces a better experience (the model can explain the limitation upfront, in its very first response, rather than after walking a user through several turns of a conversation it was never going to be able to complete) and is covered in depth in this domain's authorization lesson.

Which channel the request came in on belongs in the prompt too

The same agent talking to a user through a web chat widget and through a messaging platform like WhatsApp isn't operating under the same constraints in both places, and the prompt needs to say so. A web widget can show a rich confirm card with real buttons; a messaging channel might only support a fixed set of native UI elements, or none at all for a given action — telling the model to "walk the user through booking, then show a confirmation card" is simply wrong instruction on a channel that has no confirmation card. The fix is the same shape as the date and permissions: buildSystemPrompt({ channel: 'web' | 'whatsapp', ... }), with the channel swapping in the instructions that are actually true for what that surface can render, not a generic instruction set that quietly assumes the richer channel everywhere.

Channel affects more than prompt content, too. A web chat can hold a live connection open and stream tokens back as they're generated; a webhook-driven channel has no such connection — there's no socket to stream into, only a single request/response the platform is waiting on. That forces a second code path alongside the streaming one: same tool catalog, same underlying agent loop, but a non-streaming call that waits for the complete response before replying once. And a messaging platform never resends prior turns the way a web client re-sends its own message history on each request — so conversation history for that channel has to come from your own storage on every single turn, never assumed to arrive from the caller. Skip that, and the agent looks fine on the channel it was actually developed against, then appears to have no memory at all the moment it's wired into the second one.

The practical takeaway

Treat a system prompt like any other piece of generated output in a request-handling codebase: built fresh, from current data, on every request. Anything hardcoded into it that should have changed since the code was written is a live bug waiting for the right conversation to surface it.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. An agent's system prompt never includes the actual current date. What real-world consequence did this cause in a documented case?

2. What's the correct mental model for how a system prompt should be built in a production agent?

3. Why does stale system-prompt context matter more for an agent with write-capable tools than for a plain read-only chatbot?

4. Why is it better to include a user's current permissions directly in the system prompt, rather than relying only on which tools are technically available to the model?