Profiles API

Profiles define agent behavior — the system prompt (SKILL.md), allowed tools, MCP servers, and per-runtime overrides. Profiles are stored as YAML + Markdown on disk and support three scopes: built-in (shipped with Stagent), user (~/.claude/skills/), and project (.claude/skills/). The Profiles API covers CRUD, behavioral testing, learned context management, AI-assisted generation, and GitHub import.

Quick Start

List available profiles, run behavioral smoke tests against a runtime, and check the results:

// 1. List profiles to find ones compatible with your runtime
const profiles: Profile[] = await fetch('/api/profiles?scope=all&projectId=proj-8f3a-4b2c')
.then(r => r.json());

const claudeProfiles = profiles.filter(p =>
p.supportedRuntimes.includes('claude-code')
);
console.log(`${claudeProfiles.length} profiles support claude-code`);
claudeProfiles.forEach(p => console.log(`  ${p.name} (${p.scope}) — ${p.tags.join(', ')}`));

// 2. Run smoke tests for a profile against claude-code
const testReport: TestReport = await fetch('/api/profiles/data-analyst/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ runtimeId: 'claude-code' }),
}).then(r => r.json());

// 3. Check test results — each test sends a prompt and checks for expected keywords
console.log(`Tests: ${testReport.passed}/${testReport.total} passed`);
testReport.results.forEach(t => {
const icon = t.pass ? 'PASS' : 'FAIL';
console.log(`  [${icon}] ${t.task}`);
if (!t.pass) console.log(`    Missing: ${t.missingKeywords.join(', ')}`);
});

// 4. Retrieve persisted test results later
const savedResults: TestReport = await fetch(
'/api/profiles/data-analyst/test-results?runtimeId=claude-code'
).then(r => r.json());

Base URL

/api/profiles

Endpoints

List Profiles

GET /api/profiles

Retrieve profiles with optional scope and project filtering. Default returns user + built-in profiles. Use scope=all with a projectId to include project-scoped profiles.

Query Parameters

Param Type Req Description
scope enum Filter scope: project (project-only) or all (builtin + user + project)
projectId string Project UUID — required when scope is project or all

Response 200 — Array of profile objects sorted by name

Profile Object

FieldTypeReqDescription
idstring*Profile identifier (kebab-case slug)
namestring*Display name
descriptionstring*Brief description of the profile behavior
domainenum*work or personal
tagsstring[]*Categorization tags
skillMdstring*Full SKILL.md content (system prompt + behavioral instructions)
allowedToolsstring[]Tool allowlist for agent execution
mcpServersobjectMCP server configurations
canUseToolPolicyobjectAuto-approve/auto-deny tool policies
maxTurnsnumberMaximum agent conversation turns
outputFormatstringExpected output format
versionstring*Semantic version (x.y.z)
authorstringProfile author
sourcestring (URL)Source URL
supportedRuntimesstring[]*Compatible agent runtimes
runtimeOverridesobjectPer-runtime instruction, tool, and MCP overrides
isBuiltinboolean*Whether this is a shipped built-in profile
scopeenum*builtin, user, or project
originenumHow created: manual, environment, import, or ai-assist
readOnlyboolean*Whether the profile is read-only (true for project-scoped)

List all profiles including project-scoped ones — useful for building a profile selector when creating tasks:

// List profiles and filter by runtime compatibility
const profiles: Profile[] = await fetch('/api/profiles?scope=all&projectId=proj-8f3a-4b2c')
.then(r => r.json());

// Group by scope for display
const grouped = Object.groupBy(profiles, p => p.scope);
for (const [scope, items] of Object.entries(grouped)) {
console.log(`${scope} (${items.length}):`);
items.forEach(p => console.log(`  ${p.name} — ${p.description}`));
}

Example response:

[
  {
    "id": "data-analyst",
    "name": "Data Analyst",
    "description": "Statistical analysis with Python, pandas, and visualization best practices",
    "domain": "work",
    "tags": ["analysis", "data", "python"],
    "version": "1.2.0",
    "supportedRuntimes": ["claude-code", "anthropic-direct"],
    "isBuiltin": true,
    "scope": "builtin",
    "readOnly": true
  },
  {
    "id": "custom-reviewer",
    "name": "Custom Code Reviewer",
    "description": "Security-focused code review for the payments service",
    "domain": "work",
    "tags": ["code-review", "security"],
    "version": "1.0.0",
    "supportedRuntimes": ["claude-code"],
    "isBuiltin": false,
    "scope": "user",
    "origin": "manual",
    "readOnly": false
  }
]

Create Profile

POST /api/profiles

Create a new user profile. The request body must pass Zod validation against ProfileConfigSchema. Provide the SKILL.md content separately.

Request Body

FieldTypeReqDescription
idstring*Profile identifier (must be unique, kebab-case)
namestring*Display name
versionstring*Semantic version (x.y.z format)
domainenum*work or personal
tagsstring[]*Categorization tags
skillMdstringSKILL.md content (system prompt)
allowedToolsstring[]Tool allowlist
mcpServersobjectMCP server configurations
maxTurnsnumberMax agent turns
supportedRuntimesstring[]Compatible runtimes
testsProfileSmokeTest[]Behavioral smoke tests

Response 201 Created{ "ok": true }

Errors: 400 — Zod validation failure or duplicate ID

Create a profile with a system prompt and behavioral smoke tests:

// Create a profile with smoke tests for verification
const res: Response = await fetch('/api/profiles', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  id: 'security-reviewer',
  name: 'Security Reviewer',
  version: '1.0.0',
  domain: 'work',
  tags: ['security', 'code-review', 'owasp'],
  skillMd: 'You are a security-focused code reviewer. Check every change against the OWASP Top 10. Flag SQL injection, XSS, CSRF, and auth bypass risks. Always suggest remediations.',
  supportedRuntimes: ['claude-code', 'anthropic-direct'],
  tests: [
    {
      task: 'Review this code for SQL injection: db.query("SELECT * FROM users WHERE id = " + userId)',
      expectedKeywords: ['SQL injection', 'parameterized', 'prepared statement'],
    },
  ],
}),
});

if (res.status === 201) {
console.log('Profile created — run /test to verify behavior');
}

Get Profile

GET /api/profiles/{id}

Retrieve a single profile with scope and read-only metadata.

Response 200 — Full profile object with isBuiltin, scope, and readOnly fields

Errors: 404 — Profile not found

Fetch a profile to inspect its full SKILL.md and configuration before assigning it to a task:

// Fetch the full profile including SKILL.md content
const profile: Profile = await fetch('/api/profiles/security-reviewer')
.then(r => r.json());

console.log(`${profile.name} v${profile.version} [${profile.scope}]`);
console.log(`Runtimes: ${profile.supportedRuntimes.join(', ')}`);
console.log(`SKILL.md: ${profile.skillMd.substring(0, 100)}...`);

Update Profile

PUT /api/profiles/{id}

Replace a user profile's configuration and SKILL.md. Built-in and project-scoped profiles cannot be modified through this endpoint.

Request Body

FieldTypeReqDescription
idstring*Profile identifier (must match URL)
namestring*Display name
versionstring*Semantic version (x.y.z)
domainenum*work or personal
tagsstring[]*Categorization tags
skillMdstringUpdated SKILL.md content

Response 200{ "ok": true }

Errors: 400 — Validation failure, 403 — Built-in or project-scoped profile, 404 — Not found

Update a profile’s system prompt and bump the version:

// Update the profile and bump version
await fetch('/api/profiles/security-reviewer', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  id: 'security-reviewer',
  name: 'Security Reviewer',
  version: '1.1.0',
  domain: 'work',
  tags: ['security', 'code-review', 'owasp', 'sast'],
  skillMd: 'You are a senior security reviewer. Check all changes against OWASP Top 10 and CWE Top 25...',
}),
});

Delete Profile

DELETE /api/profiles/{id}

Permanently delete a user profile. Built-in and project-scoped profiles cannot be deleted.

Response 200{ "ok": true }

Errors: 400 — Deletion failure, 403 — Built-in or project-scoped profile

// Delete a user profile — built-ins and project profiles are protected
const res: Response = await fetch('/api/profiles/security-reviewer', { method: 'DELETE' });

if (res.status === 403) {
console.log('Cannot delete built-in or project-scoped profiles');
}

Run Profile Tests

POST /api/profiles/{id}/test

Execute all behavioral smoke tests for a profile against a specified runtime. Each test sends a task prompt and checks for expected keywords in the response. Results are persisted for later retrieval.

Request Body

FieldTypeReqDescription
runtimeIdenumAgent runtime to test against(default: claude-code)

Response 200 — Test report with per-test pass/fail results

Errors: 400 — Invalid runtime or test execution failure, 429 — Budget limit exceeded

Run all smoke tests for a profile to verify it produces the expected output keywords:

// Run all profile tests and report results
const report: TestReport = await fetch('/api/profiles/security-reviewer/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ runtimeId: 'claude-code' }),
}).then(r => r.json());

console.log(`${report.passed}/${report.total} tests passed`);
report.results.forEach(t => {
console.log(`  [${t.pass ? 'PASS' : 'FAIL'}] ${t.task.substring(0, 60)}...`);
});

Example response:

{
  "profileId": "security-reviewer",
  "runtimeId": "claude-code",
  "total": 2,
  "passed": 2,
  "results": [
    {
      "task": "Review this code for SQL injection: db.query(\"SELECT * FROM users WHERE id = \" + userId)",
      "pass": true,
      "matchedKeywords": ["SQL injection", "parameterized", "prepared statement"],
      "missingKeywords": [],
      "durationMs": 4200
    },
    {
      "task": "Check this login form for XSS vulnerabilities",
      "pass": true,
      "matchedKeywords": ["XSS", "sanitize", "escape"],
      "missingKeywords": [],
      "durationMs": 3800
    }
  ]
}

Run Single Test

POST /api/profiles/{id}/test-single

Run a single profile test by index. Used by the client for real-time progress during test execution.

Request Body

FieldTypeReqDescription
testIndexnumber*Zero-based index of the test to run
runtimeIdenumAgent runtime to test against(default: claude-code)

Response 200 — Single test result with pass/fail and keyword matches

Errors: 400 — Invalid test index or unsupported runtime, 404 — Profile not found, 429 — Budget limit exceeded

Run a single test for real-time progress feedback in the UI:

// Run tests one by one for real-time progress display
const profile: Profile = await fetch('/api/profiles/security-reviewer').then(r => r.json());
const testCount: number = profile.tests?.length ?? 0;

for (let i = 0; i < testCount; i++) {
console.log(`Running test ${i + 1}/${testCount}...`);

const result: TestResult = await fetch('/api/profiles/security-reviewer/test-single', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ testIndex: i, runtimeId: 'claude-code' }),
}).then(r => r.json());

console.log(`  [${result.pass ? 'PASS' : 'FAIL'}] ${result.durationMs}ms`);
}

Get Test Results

GET /api/profiles/{id}/test-results

Retrieve the most recent persisted test report for a profile and runtime combination.

Query Parameters

Param Type Req Description
runtimeId enum Runtime to filter results for

Response 200 — Test report object

Errors: 404 — No test results found

// Check saved test results without re-running tests
const results: TestReport = await fetch(
'/api/profiles/security-reviewer/test-results?runtimeId=claude-code'
).then(r => r.json());

console.log(`Last tested: ${results.passed}/${results.total} passed`);

Get Learned Context

GET /api/profiles/{id}/context

Retrieve the version history and size info for a profile's learned context. Learned context accumulates facts, preferences, and patterns from agent execution.

Response 200 — Version history and size information

Response Body

FieldTypeReqDescription
historyobject[]*Array of context versions with content and metadata
currentSizenumber*Current context size in bytes
maxSizenumber*Maximum allowed context size

Check how much learned context a profile has accumulated and view its version history:

// Check learned context size and version history
const ctx: ContextInfo = await fetch('/api/profiles/data-analyst/context')
.then(r => r.json());

const pct: string = ((ctx.currentSize / ctx.maxSize) * 100).toFixed(1);
console.log(`Context: ${ctx.currentSize} / ${ctx.maxSize} bytes (${pct}%)`);
console.log(`Versions: ${ctx.history.length}`);

Example response:

{
  "history": [
    {
      "version": 3,
      "content": "- User prefers seaborn over matplotlib\n- Always use pandas for CSV processing\n- Include confidence intervals in reports",
      "source": "agent",
      "createdAt": "2026-04-02T16:30:00.000Z"
    },
    {
      "version": 2,
      "content": "- User prefers seaborn over matplotlib\n- Always use pandas for CSV processing",
      "source": "operator",
      "createdAt": "2026-03-28T11:00:00.000Z"
    }
  ],
  "currentSize": 1842,
  "maxSize": 10240
}

Add Learned Context

POST /api/profiles/{id}/context

Manually add context content to a profile (operator injection). Creates a new version in the context history.

Request Body

FieldTypeReqDescription
additionsstring*Context content to add

Response 200{ "ok": true }

Errors: 400 — Missing or empty additions

Manually inject context into a profile — useful for bootstrapping preferences before the agent learns them naturally:

// Inject context manually — creates a new version
await fetch('/api/profiles/data-analyst/context', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  additions: 'Always use pandas for CSV processing. Prefer seaborn for visualizations. Include confidence intervals in statistical reports.',
}),
});

Manage Learned Context

PATCH /api/profiles/{id}/context

Approve, reject, or rollback learned context proposals. Approve and reject require a notificationId from a pending context proposal. Rollback requires a target version number.

Request Body

FieldTypeReqDescription
actionenum*approve, reject, or rollback
notificationIdstringNotification ID (required for approve/reject)
targetVersionnumberVersion number (required for rollback)
editedContentstringEdited content to replace the proposal (approve only)

Response 200{ "ok": true }

Errors: 400 — Missing required fields or invalid action

Approve an agent’s learned context proposal, or rollback to an earlier version if something went wrong:

// Approve a pending context proposal from the agent
await fetch('/api/profiles/data-analyst/context', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  action: 'approve',
  notificationId: 'notif-ctx-789',
}),
});

// Or rollback to a previous version if the latest context is wrong
await fetch('/api/profiles/data-analyst/context', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  action: 'rollback',
  targetVersion: 2,
}),
});

AI Profile Assist

POST /api/profiles/assist

Use AI to generate or refine a profile's SKILL.md, tags, and configuration from a natural language goal description. Supports generate mode (new profile) and refine mode (improve existing).

Request Body

FieldTypeReqDescription
goalstring*Natural language description of the desired profile behavior
domainenumwork or personal
modeenumgenerate (new) or refine (improve existing)(default: generate)
existingSkillMdstringCurrent SKILL.md content (for refine mode)
existingTagsstring[]Current tags (for refine mode)

Response 200 — Generated profile configuration and SKILL.md

Errors: 400 — Missing goal, 429 — Budget limit exceeded, 500 — Generation failure

Generate a complete profile from a natural language description — the AI produces a SKILL.md, tags, and configuration:

// Generate a new profile from a natural language goal
const generated: GeneratedProfile = await fetch('/api/profiles/assist', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  goal: 'A profile that reviews pull requests for security issues, focusing on OWASP Top 10 and input validation',
  domain: 'work',
  mode: 'generate',
}),
}).then(r => r.json());

console.log(`Suggested name: ${generated.name}`);
console.log(`Tags: ${generated.tags.join(', ')}`);
console.log(`SKILL.md preview: ${generated.skillMd.substring(0, 200)}...`);

// Use the generated config to create the profile
await fetch('/api/profiles', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(generated),
});

Example response:

{
  "id": "security-pr-reviewer",
  "name": "Security PR Reviewer",
  "version": "1.0.0",
  "domain": "work",
  "tags": ["security", "code-review", "owasp", "input-validation"],
  "skillMd": "You are a security-focused code reviewer specializing in pull request analysis...",
  "supportedRuntimes": ["claude-code", "anthropic-direct"],
  "allowedTools": ["Read", "Grep", "Glob", "Bash"]
}

Import from URL

POST /api/profiles/import

Import a profile from a GitHub URL. Fetches profile.yaml and optionally SKILL.md from the repository. Supports raw GitHub URLs and github.com tree/blob URLs.

Request Body

FieldTypeReqDescription
urlstring (URL)*GitHub URL to a profile directory or profile.yaml file

Response 201 Created{ "ok": true, "id": "...", "name": "..." }

Errors: 400 — Invalid URL, non-GitHub URL, fetch failure, or invalid profile.yaml

Import a profile from a GitHub repository — fetches the YAML config and SKILL.md:

// Import a profile from GitHub
const { id, name }: { id: string; name: string } = await fetch('/api/profiles/import', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  url: 'https://github.com/acme/stagent-profiles/tree/main/.claude/skills/security-reviewer',
}),
}).then(r => r.json());

console.log(`Imported "${name}" as ${id}`);

List Repo Imports

GET /api/profiles/import-repo

List all repository import records, most recent first.

Response 200 — Array of repo import records with parsed profileIds

// List all repository imports
const imports: RepoImport[] = await fetch('/api/profiles/import-repo')
.then(r => r.json());

imports.forEach(i => {
console.log(`${i.url} → ${i.profileIds.join(', ')}`);
});

Profile Config Schema

Profiles are validated with Zod. The key fields for the ProfileConfigSchema:

FieldTypeRequiredDescription
idstringYesUnique kebab-case identifier
namestringYesDisplay name
versionstring (semver)YesMust match x.y.z format
domainenumYeswork or personal
tagsstring[]YesCategorization tags
allowedToolsstring[]NoTool allowlist
mcpServersobjectNoMCP server configurations
canUseToolPolicyobjectNo{ autoApprove?: string[], autoDeny?: string[] }
maxTurnsnumberNoMaximum agent conversation turns
supportedRuntimesenum[]NoArray of runtime IDs
preferredRuntimeenumNoPreferred runtime for auto-routing
runtimeOverridesobjectNoPer-runtime instruction/tool/MCP overrides
capabilityOverridesobjectNoPer-runtime model, thinking, and server tool overrides
testsobject[]No{ task: string, expectedKeywords: string[] }[]

Supported Runtimes

Runtime IDLabel
claude-codeClaude Code
openai-codex-app-serverOpenAI Codex App Server
anthropic-directAnthropic Direct API
openai-directOpenAI Direct API
ollamaOllama (Local)