data/skills/n8n-agents/SYSTEM_PROMPT.md
The system prompt is the load-bearing config of an agent. Most "the agent isn't doing what I want" problems trace back to a system prompt that's too long, too vague, or mixing concerns.
This file is opinionated: keep system prompts on persona and global behavior, push tool-specific instructions into tool descriptions, and iterate. The system prompt goes in options.systemMessage on the agent node.
![]() markdown"), language.What it is NOT for: per-tool usage instructions. Those go in the tool's description.
A hardcoded date is stale immediately. Inject it at runtime:
Current date: {{ $now }}
or formatted:
The current time is {{ $now.format('DDDD TTTT') }}
System prompt → Persona, global behavior, format rules, file handling
Tool description → How to use THIS tool, its parameters, when to pick it over others
$fromAI desc. → What value to put in this specific parameter
Why this split:
| Was in the system prompt | Better location |
|---|---|
"When using Generate Image, prefer realistic photography over 8k cinematic" | Generate Image tool description |
| "When the user uploads an image and asks for background changes, edit it, don't generate new" | Edit Image tool description (and a "do not use" boundary on Generate Image) |
| "Use 9:16 aspect ratio for video tools" | Generate Video tool description |
"Respond with markdown image embeds: " | System prompt (global display rule) |
| "Refuse to generate images of real people without consent" | System prompt (global safety) |
| "Today is 2026-04-25" | System prompt as {{ $now }} (universal context, computed) |
The first three move out; the last three stay in.
Inline (typed directly into systemMessage) is fine for a first agent or any prompt that lives in one place. A 1500-token inline prompt is a normal shape — don't push first-time builders toward externalization.
The real reason to externalize is piecing, not length. Reusable chunks of context — COMPANY_DESCRIPTION, BRAND_VOICE, CURRENT_PROMOTION — each get one canonical home, and every prompt that needs them references that home. Suggest this when you see one of:
COMPANY_DESCRIPTION quarterly, CURRENT_PROMOTION weekly).If none apply, stay inline. Mid-prompt restructures cost more than they save with no second consumer to pay them back.
Load each chunk at workflow start (one node per chunk — a Data Table Get Row, an HTTP fetch, a Set node), then reference them inline in systemMessage where they should appear:
=You are the assistant for {{ $('Company Description').first().json.value }}.
## Market positioning
{{ $('Market Fit').first().json.value }}
## Brand voice
{{ $('Brand Voice').first().json.value }}
Current date: {{ $now }}
User: {{ $('Lookup').first().json.name }}
Mix sources: a Data Table (default for shared chunks, editable in UI), n8n Variables ($vars.X, paid plans — short shared values like a brand name), or computed at run time ($now, current user, available files).
Treat the system prompt like code:
Most "the agent doesn't follow my instructions" issues are conflicts between the system prompt, tool descriptions, and model defaults. Resolve those conflicts first.
| Anti-pattern | Symptom | Fix |
|---|---|---|
| "You are a helpful assistant" + no specifics | Generic responses, no identity | Replace with a specific role and scope |
| 5000-token prompt with a section per tool | Token cost, slow responses, hard to edit | Move tool sections to tool descriptions |
| Hardcoded date / "current year" | Stale immediately | Inject {{ $now }} at runtime |
A stack of DON'T rules | Model gets defensive, refuses too eagerly | Frame as positive instructions where possible |
| Multiple pasted "examples" | Cargo-cult, rarely earns its tokens | One sharp example, or none |
| Per-execution context hardcoded | Hard to update | Build the prompt from a template + variables |