Operations

Profiles

Auto-maintained entity profiles aggregated from memory and knowledge.

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.

Note

Spectron was the project name for SurrealDB Agent Memory. These type names
will be renamed in a future release.

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.

A profile is divided into four sections, each drawing from a different category of memory.

static contains the entity's stable biographical and organisational facts - identity-category attributes and any resolved authoritative knowledge. These change rarely and carry no decay.

dynamic contains the entity's recent situational context - context-category attributes from the current and recent sessions. This section reflects what the entity is doing right now.

preferences contains knowledge-category attributes that describe how the entity likes things to work. Editor preferences, communication style, tool choices, and opinion-based attributes appear here.

instructions contains all active instruction records scoped to this entity. These are directives the entity or a managing principal has given about how the agent should behave.

SectionSourceVolatility
staticIdentity attributes + authoritative knowledgeLow - no decay
dynamicContext attributesHigh - decays at 0.95/day
preferencesKnowledge attributesMedium - decays at 0.995/day
instructionsInstruction table (active = true)Varies
profile = await memory.profile()

print(profile.static)        # list of {"key": ..., "value": ...}
print(profile.dynamic)       # list of {"key": ..., "value": ...}
print(profile.preferences)   # list of {"key": ..., "value": ...}
print(profile.instructions)  # list of {"id":…, "label":…, "description":…}
import { Spectron } from "@surrealdb/spectron";

const memory = new Spectron({ context: "ctx_01jt4kx...", apiKey: "sk-..." });

const profile = await memory.profile();

console.log(profile.static);
console.log(profile.dynamic);
console.log(profile.preferences);
console.log(profile.instructions);
GET /api/v1/{context_id}/profile
{
  "static": [
    { "key": "name", "value": "Alice Chen" },
    { "key": "role", "value": "CTO" },
    { "key": "organisation", "value": "Acme Corp" },
    { "key": "timezone", "value": "Europe/London" }
  ],
  "dynamic": [
        { "key": "current_focus",
        "value": "Platform reliability roadmap" },
    { "key": "recent_decision", "value": "Chose a relational database for the new analytics service" }
  ],
  "preferences": [
    { "key": "editor", "value": "Cursor" },
    { "key": "language", "value": "TypeScript" },
    { "key": "response_style", "value": "Concise, no filler phrases" }
  ],
  "instructions": [
        { "label": "Language",
        "description": "Always respond in British English" },
        { "label": "Code examples",
        "description": "Show TypeScript first, Python second" }
  ]
}

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)}`;

A profile is organised by memory category (static, dynamic, preferences, instructions) - not by which entity each attribute belongs to. When you mention yourself, your school, and your cat in one conversation, all extracted attributes that match the requested scope can appear in the same section. For example, date_of_birth (you), school_level (your school), and species (your cat) may sit side by side under static or preferences.

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.

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.

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.

Was this page helpful?