SOUL: The system prompt
Before the code: what identity and tools mean for an assistant
Right now your assistant is a smart parrot. It answers anything in any tone. To turn it into someone, you give it an identity: a short paragraph at the start of every conversation that says who it is, who it is talking to, and how it should behave. Then to turn it from a chatter into a doer, you give it tools: normal Python functions like read_file or run_command that the model can ask you to run on its behalf. Identity tells the assistant what to be. Tools let it do real work.
Identity and tools in one picture
Quiz: Quiz
Loading practice…
Right now the bot has no personality. It will say anything, in any tone, with any pronoun. The fix is a system prompt: a special first message that the model reads as instructions instead of conversation. We call ours SOUL.
# OpenClaw
You are OpenClaw, a personal AI assistant.
## Personality
- Helpful, concise, and technically competent
- Friendly but professional tone
- When uncertain, say so rather than guessing
## Boundaries
- Do not pretend to browse the internet
- Do not invent information
## Memory Strategy
- Notice facts the user shares about themselves
- Reference past context naturally
- If corrected, update your understanding immediatelyA plain markdown file. Easy to read, easy to diff. This is the only place the bot defines who it is.
In the OpenAI / OpenRouter Chat Completions shape, the system prompt is the first message in the array with role=system. We keep it OUTSIDE the saved history so it never gets summarised, never gets duplicated, and you can edit SOUL.md without rewriting old sessions.
def reply_with_soul(user_id: str, user_text: str) -> str:
"""Call the LLM with SOUL as system prompt + persisted history."""
history = load_session(user_id)
user_msg = {"role": "user", "content": user_text}
history.append(user_msg)
append_message(user_id, user_msg)
# Prepend the system prompt, never saved to disk
messages = [{"role": "system", "content": SOUL}] + history
response = client.chat.completions.create(
model=MODEL,
max_tokens=1024,
messages=messages,
)
reply = response.choices[0].message.content or ""
assistant_msg = {"role": "assistant", "content": reply}
append_message(user_id, assistant_msg)
return replyload_session returns the persisted user/assistant turns. We prepend a fresh system message on every call. SOUL stays in memory, never in the JSONL log.
Where SOUL lives at runtime
Three reasons. First, when you edit SOUL.md to refine the personality, every old session would still carry the old SOUL. Second, when we add context compaction, summarising old turns might drop SOUL out of the picture. Third, SOUL is identity, not a turn the user said. Keeping it separate matches the way the model thinks about role=system.
Quiz: Quiz
Loading practice…
AI prompt: Try it: rewrite SOUL
Loading practice…