Agent Profiles
A profile is a saved, versioned bundle of agent configuration — system prompt, model, sampling parameters, MCP tools, prompt variables — that a conversation points at by id instead of re-sending the same generation config on every call.
Two things follow from that. Editing a profile changes every conversation using it, with no client release. And because each edit appends an immutable version, you can read back exactly what a turn ran against months later.
This guide is the write side: creating, editing, versioning and deleting profiles, plus the prompt fragments they compose. Pointing a conversation at a profile lives on the LLM APIs and is covered in Configuration.
Profiles are tenant configuration, not per-user data, so they take a secret key
on its own — no X-On-Behalf-Of header, no end-user JWT. That is the opposite of
the LLM, storage and end-user APIs, where a bare sk_… key returns
401 authenticated user_id is required.
Every profile also lives in a project, addressed with the optional X-Project-Id
header. Until Projects ships, default is the only valid value and is what you get when
the header is omitted.
Create a Profile
curl -X POST https://api.travila.ai/api/v1/agent-profiles/create \
-H "X-API-Key: sk_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"profile": {
"profileId": "nutrition_coach",
"name": "Nutrition Coach",
"description": "Food, meals and macros.",
"whenToUse": "Use when the user asks about food, meals, or macros.",
"keywords": ["nutrition", "food", "macros"],
"enabled": true,
"generationConfig": {
"model": "google/gemini-3.6-flash",
"systemPrompt": "You are a nutrition coach. Be concise and practical.",
"temperature": 0.4
}
}
}'
Response: the stored record, echoed back with version: 1.
{
"profile": {
"profileId": "nutrition_coach",
"name": "Nutrition Coach",
"description": "Food, meals and macros.",
"whenToUse": "Use when the user asks about food, meals, or macros.",
"keywords": ["nutrition", "food", "macros"],
"enabled": true,
"generationConfig": {
"model": "google/gemini-3.6-flash",
"systemPrompt": "You are a nutrition coach. Be concise and practical.",
"temperature": 0.4
},
"version": 1
}
}
profileId must be unique in the project — a duplicate fails ALREADY_EXISTS rather
than overwriting. Any version you send is ignored; the store assigns 1.
Writing whenToUse
whenToUse is read by the router model, not by a human browsing a list. Write it as
an instruction to a reader deciding whether this profile fits the next turn:
- Good:
Use when the user asks about food, meals, macros, or logging what they ate. - Poor:
The nutrition profile.
description and keywords are the human-facing and lexical surfaces. Together with
profileId, name and enabled, these are the only fields the router ever sees — see
the library.
Read a Profile
curl -X POST https://api.travila.ai/api/v1/agent-profiles/get \
-H "X-API-Key: sk_your_key_here" \
-H "Content-Type: application/json" \
-d '{"profileId": "nutrition_coach"}'
Response: {"profile": {...}} — the full record.
Pass version to read a specific immutable version instead of the latest:
{
"profileId": "nutrition_coach",
"version": 3
}
Omitting version (or sending 0) gives you the latest. A version that never existed
returns NOT_FOUND.
List Profiles
curl -X POST https://api.travila.ai/api/v1/agent-profiles/list \
-H "X-API-Key: sk_your_key_here" \
-H "Content-Type: application/json" \
-d '{"page": 1, "pageSize": 25}'
Response:
{
"profiles": [{"profileId": "nutrition_coach", "name": "Nutrition Coach", "version": 3}],
"totalCount": 1,
"hasMore": false
}
Disabled profiles are excluded unless you send includeDisabled: true. This returns
full records — system prompts, tool config, the lot — so for anything that just
needs to know what exists, use the library instead.
Read the Selection Library
The library is the project's selection surface: id, name, description, whenToUse,
keywords and enabled flag, plus the project's defaultProfileId. No system prompts, no
generation config, no tools.
curl -X POST https://api.travila.ai/api/v1/agent-profiles/library \
-H "X-API-Key: sk_your_key_here" \
-H "Content-Type: application/json" \
-d '{}'
Response:
{
"library": {
"profiles": [
{
"profileId": "nutrition_coach",
"name": "Nutrition Coach",
"description": "Food, meals and macros.",
"whenToUse": "Use when the user asks about food, meals, or macros.",
"keywords": ["nutrition", "food", "macros"],
"enabled": true
}
],
"defaultProfileId": "generalist"
}
}
This is exactly what the router model sees when choosing a profile for a turn, and the
cheap way to enumerate what exists — use it to populate a picker rather than paging
list.
defaultProfileId is the generalist fallback used when no profile is active and the
router picks none. An empty value means fall back to the conversation's own generation
config.
Update a Profile
Updates are partial: only the fields named in updateMask are written, and each
successful update appends a new immutable version.
curl -X POST https://api.travila.ai/api/v1/agent-profiles/update \
-H "X-API-Key: sk_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"profileId": "nutrition_coach",
"profile": {
"generationConfig": {
"model": "anthropic/claude-sonnet-4",
"temperature": 0.2
}
},
"updateMask": "generationConfig"
}'
Response: {"profile": {...}} with the new version.
updateMask replaces the whole recordWithout a mask the supplied profile becomes the stored record wholesale — every field
you did not send is cleared. Send the mask unless a full replace is what you mean.
Disabling is an ordinary update, and is the reversible alternative to deleting:
{
"profileId": "nutrition_coach",
"profile": {"enabled": false},
"updateMask": "enabled"
}
Version History
curl -X POST https://api.travila.ai/api/v1/agent-profiles/versions \
-H "X-API-Key: sk_your_key_here" \
-H "Content-Type: application/json" \
-d '{"profileId": "nutrition_coach"}'
Response: newest first, one entry per successful mutation.
{
"versions": [
{"version": 3, "createdAt": "2026-08-14T10:22:41Z"},
{"version": 2, "createdAt": "2026-07-02T16:04:09Z"},
{"version": 1, "createdAt": "2026-06-19T09:11:55Z"}
]
}
Pair a version number with get to read exactly what a past turn ran
against — the reason to record the version alongside a trace. A profile that never
existed returns an empty list rather than an error.
There is no rollback endpoint. To revert, get the old version and update with its
contents; that appends a new version rather than rewriting history.
Delete a Profile
curl -X POST https://api.travila.ai/api/v1/agent-profiles/delete \
-H "X-API-Key: sk_your_key_here" \
-H "Content-Type: application/json" \
-d '{"profileId": "nutrition_coach"}'
Response: empty body on success.
Deleting is not scoped to unused profiles, and there is no reference check:
- If the profile was the project's
defaultProfileId, that pointer is cleared. - Conversations still naming it in
activeProfileIdfall back to their own generation config on subsequent turns — they do not error. - Version history goes with it.
Prefer enabled: false when you only want it out of the router's reach.
Import an Existing Prompt Library
If your prompts already live in a file-based library — personas plus shared blocks they
@include — convert the whole thing in one call rather than hand-porting each one.
curl -X POST https://api.travila.ai/api/v1/agent-profiles/import \
-H "X-API-Key: sk_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"templates": [
{
"profileId": "nutrition_coach",
"name": "Nutrition Coach",
"whenToUse": "Use when the user asks about food, meals, or macros.",
"keywords": ["nutrition"],
"sourceDsl": "@include _shared/safety.txt\n\nYou are a nutrition coach for {{user_name}}.",
"generationConfig": {"model": "google/gemini-3.6-flash"}
}
],
"fragments": [
{
"fragmentId": "safety",
"name": "Safety rules",
"path": "_shared/safety.txt",
"sourceDsl": "Never give medical advice."
}
],
"emitFragments": true
}'
Response:
{
"profiles": [{"profileId": "nutrition_coach", "version": 1}],
"fragments": [{"fragmentId": "safety", "name": "Safety rules"}]
}
Conversion happens at import time — the platform never interprets your source DSL
when serving a turn. The importer rewrites prompts to the platform template syntax and
derives variableSpecs from how each variable is used; variableTypes overrides a type
it would otherwise infer.
Flatten or fragments
emitFragments | What happens | Choose it when |
|---|---|---|
false (default) | Each @include is resolved inline into that profile's system prompt. No fragment rows are written. | You want profiles self-contained and are happy editing shared text in several places. |
true | Shared blocks become fragment rows, referenced with {{template "id" .}}. | You want one edit to reach every profile. Every block referenced by an @include must be supplied in the same call. |
Rows that already exist cause ALREADY_EXISTS unless you send overwrite: true.
Related
- Prompt Fragments — the shared blocks profiles compose
- Conversation Configuration — selecting a profile for a conversation
- MCP Tools — the tool bundle a profile carries
- Evals — scoring what a profile produced