A profile is a computed view over the memory store for a given entity - typically a user. It aggregates the entity's most relevant attributes, preferences, and active instructions into a structured object that can be injected directly into an LLM system prompt.
Profiles are not a separate store. They are assembled on demand from the same attribute, instruction, and knowledge tables that back all other SurrealDB Agent Memory operations. There is no synchronisation lag and no risk of a profile drifting out of step with the underlying data.
Sections
A profile is divided into five sections, each drawing from a different category of memory.
selfFacts contains the attributes of the calling principal's own self entity, whatever their memory category. Facts about the user themselves land here, so they are not mixed with facts about other people, places, or things the user mentioned.
static contains identity-category attributes of every other entity in the caller's read region: stable biographical and organisational facts. These change rarely and carry no decay.
dynamic contains context-category attributes of other entities: recent situational context from the current and recent sessions.
preferences contains knowledge-category attributes of other entities. Editor preferences, communication style, tool choices, and opinion-based attributes often appear here.
instructions contains all active instruction records in the caller's read region. These are directives the user or a managing principal has given about how the agent should behave.
| Section | Source | Volatility |
|---|---|---|
selfFacts | Attributes of the caller's self entity, any category | Follows each attribute's category |
static | Identity attributes of other entities | Low - no decay |
dynamic | Context attributes of other entities | High - decays at 0.95/day |
preferences | Knowledge attributes of other entities | Medium - decays at 0.995/day |
instructions | Instruction table (active = true) | Varies |
Each entry in the attribute sections carries entity and entityType, naming the entity the attribute belongs to, alongside key and value.
Fetching a profile
profile = await memory.profile()
print(profile.static) # list of entries with key and value
print(profile.dynamic) # list of entries with key and value
print(profile.preferences) # list of entries with key and value
print(profile.instructions) # list of {"id":…, "label":…, "description":…}The Python SDK's ProfileResponse does not include selfFacts or the entity and entityType fields yet. Read them through the REST endpoint or the TypeScript SDK.
import { AgentMemory } from "@surrealdb/memory";
const memory = new AgentMemory({ context: "ctx_01jt4kx...", apiKey: "sp-..." });
const profile = await memory.profile();
console.log(profile.selfFacts);
console.log(profile.static);
console.log(profile.dynamic);
console.log(profile.preferences);
console.log(profile.instructions);REST
GET /api/v1/{context_id}/profile{
"selfFacts": [
{ "entity": "Alice Chen", "entityType": "person", "key": "role", "value": "CTO" },
{ "entity": "Alice Chen", "entityType": "person", "key": "timezone", "value": "Europe/London" }
],
"static": [
{ "entity": "Acme Corp", "entityType": "organisation", "key": "headquarters", "value": "London" }
],
"dynamic": [
{ "entity": "Analytics service", "entityType": "project", "key": "current_focus", "value": "Platform reliability roadmap" }
],
"preferences": [
{ "entity": "Analytics service", "entityType": "project", "key": "language", "value": "TypeScript" }
],
"instructions": [
{ "id": "01k7e2q4xc", "label": "Language", "description": "Always respond in British English" },
{ "id": "01k7e2q4xd", "label": "Code examples", "description": "Show TypeScript first, Python second" }
]
}Injecting profiles into system prompts
The profile object is designed to be formatted directly into an LLM system prompt. A typical integration looks like this:
profile = await memory.profile()
def format_profile(profile) -> str:
lines = ["## User profile"]
if profile.static:
lines.append("\n### About")
for item in profile.static:
lines.append(f"- {item['key'].replace('_', ' ').title()}: {item['value']}")
if profile.dynamic:
lines.append("\n### Current context")
for item in profile.dynamic:
lines.append(f"- {item['key'].replace('_', ' ').title()}: {item['value']}")
if profile.preferences:
lines.append("\n### Preferences")
for item in profile.preferences:
lines.append(f"- {item['value']}")
if profile.instructions:
lines.append("\n### Instructions")
for inst in profile.instructions:
lines.append(f"- {inst['description']}")
return "\n".join(lines)
system_prompt = f"You are a helpful assistant.\n\n{format_profile(profile)}"function formatProfile(profile: Profile): string {
const lines = ["## User profile"];
if (profile.static.length > 0) {
lines.push("\n### About");
for (const item of profile.static) {
lines.push(`- ${item.key}: ${item.value}`);
}
}
if (profile.dynamic.length > 0) {
lines.push("\n### Current context");
for (const item of profile.dynamic) {
lines.push(`- ${item.key}: ${item.value}`);
}
}
if (profile.preferences.length > 0) {
lines.push("\n### Preferences");
for (const item of profile.preferences) {
lines.push(`- ${item.value}`);
}
}
if (profile.instructions.length > 0) {
lines.push("\n### Instructions");
for (const inst of profile.instructions) {
lines.push(`- ${inst.description}`);
}
}
return lines.join("\n");
}
const systemPrompt = `You are a helpful assistant.\n\n${formatProfile(profile)}`;Category grouping, not entity grouping
A profile is organised by memory category (static, dynamic, preferences, instructions) - not by which entity each attribute belongs to. The one exception is selfFacts, which holds the caller's own attributes. When you mention yourself, your school, and your cat in one conversation, your date_of_birth goes to selfFacts, while school_level (your school) and species (your cat) may sit side by side under static or preferences. Each entry's entity field says which entity it belongs to.
That flat shape is deliberate for prompt injection - one dense briefing block for the model. For per-subject inspection, use the Entities view in SurrealDB Studio, GET /entities, or entity-scoped reads rather than the profile alone.
Scope filtering
GET /profile takes no scope parameter. The profile always covers everything the calling key can read, so the shape of a profile is determined by the key's memory:read grants, not by a per-request argument.
# Whatever this key can read
profile = await memory.profile()To build different profile shapes for different agent roles, give each role a key whose read region matches it. A customer-facing agent holding a key granted org/acme/user/alice receives that user's full profile; an administrative agent holding a key granted only org/acme receives org-scoped context without any individual user's personal data.
Profile versus context
Profiles and context queries serve overlapping but distinct purposes.
memory.context() is query-driven: you describe what you need to know and SurrealDB Agent Memory retrieves the most relevant attributes for that query. Use it when the agent's question determines what memory is relevant.
memory.profile() is entity-driven: you request everything SurrealDB Agent Memory knows about an entity, organised by category. Use it when you want a comprehensive briefing on a user at the start of a session, before any specific query has been issued.
In practice, many integrations combine both: inject the profile at the start of the system prompt for background context, then call memory.context() at query time to surface specific relevant memories.