This is the core of avatar configuration: what the avatar knows about itself, and who gets to change it.
Edit in the UI → it becomes the avatar’s default, stored and versioned. Pass as an API parameter → it overrides for that session only, and is never stored.
Both write to the same three fields — system prompt, backstory, tools — plus the config group. They differ in durability, not in power:
| Where it is written | How long it lasts | Visible in the dashboard | |
|---|---|---|---|
| Dashboard edit | A new profile row | Until you change the default again | Yes — with a version history |
POST /api/v1/avatars/:id/profiles |
A new profile row | Same — this is the UI’s API | Yes |
Mint parameter (context.*, tools, config) |
Nowhere | One session | No |
An API caller can do either. POST …/profiles is the “make this the new default” call; the mint
parameters are the “just for this conversation” call. Reaching for the wrong one is the mistake
this page exists to prevent.
Every session composes one system prompt on the GPU box, in this order:
[Core] Casola-owned safety and speech rules — always present, never overridable
<persona> the behaviour prompt ← system_prompt
[Backstory] who this character is ← backstory
[Tools] callable tool definitions ← tools
[Memory] per-session memory block ← context.memory_block
[Language] reply-language preference ← response_language
[Core] is owned by the box and ships with its code: reply length, no markdown, mirror the user’s
language, safety rules. Nothing you send can weaken or replace it. That is what makes it safe to
expose prompt editing to end users in a first-party UI.
For each of system_prompt, backstory and tools, independently:
1. the session parameter (context.system_prompt / context.backstory / tools)
2. the profile (pinned via profile_id, else the avatar's default profile)
3. the sealed version (what the avatar was created with)
The first layer that has a value wins. Layers replace, they do not stack.
Resolution is per field. Overriding one field leaves the others resolving normally:
{ "avatar_id": "019fe463-…", "context": { "backstory": "A support specialist at Acme." } }
→ backstory from the session, system prompt from the profile, tools from the profile. Not “session layer wins everything”.
Given avatar Lyra:
| Layer | system_prompt |
backstory |
tools |
|---|---|---|---|
| Sealed version (created by the wizard) | “You are Lyra…” | “A former archivist…” | — |
| Default profile (edited in the UI) | “You are Lyra, Acme’s guide.” | — | lookup_order |
| Session parameters | — | “Speaking with a returning customer.” | — |
Resolved for this session:
| Field | Value | source |
|---|---|---|
system_prompt |
“You are Lyra, Acme’s guide.” | profile |
backstory |
“Speaking with a returning customer.” | session |
tools |
lookup_order |
profile |
Note the backstory: the profile had none, so without the session parameter it would have fallen through to the version’s “A former archivist…”. Falling through is per field and goes all the way down.
Two different jobs, two different parameter families. This is the second most common mistake after confusing defaults with overrides.
| Parameter | Effect | Cap | |
|---|---|---|---|
| Replace | context.system_prompt |
Discards the profile/version prompt entirely | 40,960 |
context.backstory |
Discards the profile/version backstory | 16,384 | |
tools |
Discards stored tool config | — | |
| Add | extra_system_prompt |
Appended after whatever won | 2,048 |
extra_backstory |
Appended to the backstory block | 2,048 | |
extra_tools |
Merged into whatever tools resolved | — |
If you want “the avatar as authored, plus one sentence of context about this caller”, you want the extras — not a replacement:
{
"avatar_id": "019fe463-…",
"extra_system_prompt": "The caller is Dana, a Pro customer since 2024. Their last ticket was about billing.",
"extra_backstory": "You and Dana have spoken twice before."
}
Restating the whole persona just to append a sentence is the anti-pattern extras exist to remove — it also costs you first-turn latency (see Performance). Combined extras are capped at 3,072 characters because they ride inside the session token itself.
Use a replacement when the avatar genuinely plays a different role this session — the same face and voice hosting a different scripted scenario, for example.
A profile is a row holding system_prompt, backstory, tools_json and config_json. Profiles
are append-only and hash-deduped: saving identical content returns the existing row rather
than creating a duplicate. An avatar’s default_profile_id is what every mint without a
profile_id resolves to.
Profiles are cheap by design. Editing text does not re-seal the avatar’s bundle, does not
change its content_hash, and does not make the GPU fleet re-download anything — which is exactly
why prompt editing is a different operation from publishing a new version.
curl -sS -X POST https://api.casola.ai/api/v1/avatars/019fe463-…/profiles \
-H "Authorization: Bearer $CASOLA_SECRET_KEY" \
-H "content-type: application/json" \
-d '{
"system_prompt": "You are Lyra, Acmes product guide. Be concise and concrete.",
"backstory": "A former archivist who now helps people find things.",
"tools_json": { "tools": [ { "name": "lookup_order", "description": "Look up an order by id" } ] },
"config_json": { "greeting_enabled": true, "greeting_hold_ms": 2000 },
"set_default": true
}'
{
"id": "019fe4c1-2b7a-7000-8044-9e1d0f7a5c33",
"avatar_id": "019fe463-fbd3-7000-80e8-85dad81d3fee",
"hash": "a2271f20509d8c…",
"hash_short": "a2271f20509d",
"created_at": 1786249001,
"is_default": true,
"created": true
}
| Detail | Behaviour |
|---|---|
set_default: true |
Makes it the avatar’s default — the dashboard sends this by default |
created: true + 201 |
A genuinely new profile |
created: false + 200 |
Identical content already existed; you got that row back |
| Omitted field | Stored as null — meaning “fall through to the version”, not “empty string” |
The 200 vs 201 distinction is what lets an editor tell the user “no changes — this is already
version a2271f20509d” instead of silently appearing to save nothing.
| Route | Method | Purpose |
|---|---|---|
/api/v1/avatars/:avatarId/profiles |
GET |
List (metadata + sizes, no bodies) + default_profile_id |
/api/v1/avatars/:avatarId/profiles/:profileId |
GET |
One profile with its text bodies |
/api/v1/avatars/:avatarId/profiles |
POST |
Append (above) |
/api/v1/avatars/:avatarId/profiles/:profileId/default |
POST |
Switch the default |
/api/v1/avatars/:avatarId/profiles/:profileId |
DELETE |
Delete a non-default profile |
The list route returns sizes rather than bodies — a list of forty 40 KB prompts is not a list. Read one profile to prefill an editor.
// GET /api/v1/avatars/:avatarId/profiles
{
"data": [
{
"id": "019fe4c1-2b7a-7000-8044-9e1d0f7a5c33",
"hash": "a2271f20509d8c…",
"hash_short": "a2271f20509d",
"created_at": 1786249001,
"created_by": "usr_3f21…",
"is_default": true,
"sizes": { "system_prompt": 812, "backstory": 240, "tools_json": 318, "config_json": 46 }
}
],
"default_profile_id": "019fe4c1-2b7a-7000-8044-9e1d0f7a5c33"
}
// GET /api/v1/avatars/:avatarId/profiles/:profileId — same fields plus the bodies
{
"id": "019fe4c1-…",
"avatar_id": "019fe463-…",
"hash_short": "a2271f20509d",
"is_default": true,
"sizes": { … },
"system_prompt": "You are Lyra, Acmes product guide. Be concise and concrete.",
"backstory": "A former archivist who now helps people find things.",
"tools_json": { "inline": [ … ] },
"config_json": { "greeting_enabled": true }
}
Switching the default is how you roll back a prompt change: profiles are immutable, so the previous one is still there.
curl -sS -X POST https://api.casola.ai/api/v1/avatars/019fe463-…/profiles/019fe4a0-…/default \
-H "Authorization: Bearer $CASOLA_SECRET_KEY"
Deleting the current default is refused (409 profile_is_default) — switch first, then delete.
Otherwise the avatar would silently fall back to its sealed version’s text, which looks like data
loss to whoever is talking to it.
{ "avatar_id": "019fe463-…", "profile_id": "019fe4a0-…" }
Runs a non-default profile without changing anything stored — A/B testing a prompt, or a “chat with
this version” affordance in your own UI. The profile must belong to the resolved avatar
(400 profile_mismatch): asking for specific text and silently getting different text is precisely
the failure profiles exist to remove.
Their profile #1 is synthesized lazily from their published version’s text on first read or mint. You never see an avatar with no profile; you may see one whose profile was created later than the avatar.
Every mint response echoes the text layer it resolved:
"profile": { "id": "019fe4c1-…", "hash_short": "a2271f20509d", "source": "default" }
source |
Meaning |
|---|---|
param |
You pinned it with profile_id |
default |
The avatar’s default profile |
none |
No profile — the sealed version’s own text ran |
Log hash_short alongside your own session records and “which prompt was this user talking to?”
stays answerable months later. The box additionally logs a system_prompt_final record with the
full composed text, its SHA, and the winning layer per field.
| Field | Cap (characters) |
|---|---|
context.system_prompt / profile system_prompt |
40,960 (~10k tokens) |
context.backstory / profile backstory |
16,384 |
extra_system_prompt |
2,048 |
extra_backstory |
2,048 |
| Combined extras | 3,072 |
context.memory_block |
4,096 |
context.history |
40 messages / 16,384 bytes |
config (serialized) |
1,024 bytes |
Exceeding a cap on a session parameter is a 400 — it is your input, so you should hear about
it. Exceeding it on a stored profile clamps at mint with a server-side warning rather than
failing the session, because stored data may have been authored by someone else at another time and
a live call is the wrong place to discover it.
The composed prompt is re-sent to the language model on every turn. The [Core] block and the
avatar’s own bundled persona are shared across sessions and stay warm in the model’s prefix cache;
a session-level override breaks that sharing beyond [Core] for that session, so the first turn
pays extra prefill proportional to the prompt size. Later turns in the same session are cached.
Practical guidance:
extra_system_prompt over restating the persona — it is small by construction and keeps
the shared prefix intact.