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
/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
| Field | Type | Req | Description |
|---|---|---|---|
| id | string | * | Profile identifier (kebab-case slug) |
| name | string | * | Display name |
| description | string | * | Brief description of the profile behavior |
| domain | enum | * | work or personal |
| tags | string[] | * | Categorization tags |
| skillMd | string | * | Full SKILL.md content (system prompt + behavioral instructions) |
| allowedTools | string[] | — | Tool allowlist for agent execution |
| mcpServers | object | — | MCP server configurations |
| canUseToolPolicy | object | — | Auto-approve/auto-deny tool policies |
| maxTurns | number | — | Maximum agent conversation turns |
| outputFormat | string | — | Expected output format |
| version | string | * | Semantic version (x.y.z) |
| author | string | — | Profile author |
| source | string (URL) | — | Source URL |
| supportedRuntimes | string[] | * | Compatible agent runtimes |
| runtimeOverrides | object | — | Per-runtime instruction, tool, and MCP overrides |
| isBuiltin | boolean | * | Whether this is a shipped built-in profile |
| scope | enum | * | builtin, user, or project |
| origin | enum | — | How created: manual, environment, import, or ai-assist |
| readOnly | boolean | * | 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
/api/profiles Create a new user profile. The request body must pass Zod validation against ProfileConfigSchema. Provide the SKILL.md content separately.
Request Body
| Field | Type | Req | Description |
|---|---|---|---|
| id | string | * | Profile identifier (must be unique, kebab-case) |
| name | string | * | Display name |
| version | string | * | Semantic version (x.y.z format) |
| domain | enum | * | work or personal |
| tags | string[] | * | Categorization tags |
| skillMd | string | — | SKILL.md content (system prompt) |
| allowedTools | string[] | — | Tool allowlist |
| mcpServers | object | — | MCP server configurations |
| maxTurns | number | — | Max agent turns |
| supportedRuntimes | string[] | — | Compatible runtimes |
| tests | ProfileSmokeTest[] | — | 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
/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
/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
| Field | Type | Req | Description |
|---|---|---|---|
| id | string | * | Profile identifier (must match URL) |
| name | string | * | Display name |
| version | string | * | Semantic version (x.y.z) |
| domain | enum | * | work or personal |
| tags | string[] | * | Categorization tags |
| skillMd | string | — | Updated 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
/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
/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
| Field | Type | Req | Description |
|---|---|---|---|
| runtimeId | enum | — | Agent 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
/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
| Field | Type | Req | Description |
|---|---|---|---|
| testIndex | number | * | Zero-based index of the test to run |
| runtimeId | enum | — | Agent 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
/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
/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
| Field | Type | Req | Description |
|---|---|---|---|
| history | object[] | * | Array of context versions with content and metadata |
| currentSize | number | * | Current context size in bytes |
| maxSize | number | * | 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
/api/profiles/{id}/context Manually add context content to a profile (operator injection). Creates a new version in the context history.
Request Body
| Field | Type | Req | Description |
|---|---|---|---|
| additions | string | * | 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
/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
| Field | Type | Req | Description |
|---|---|---|---|
| action | enum | * | approve, reject, or rollback |
| notificationId | string | — | Notification ID (required for approve/reject) |
| targetVersion | number | — | Version number (required for rollback) |
| editedContent | string | — | Edited 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
/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
| Field | Type | Req | Description |
|---|---|---|---|
| goal | string | * | Natural language description of the desired profile behavior |
| domain | enum | — | work or personal |
| mode | enum | — | generate (new) or refine (improve existing)(default: generate) |
| existingSkillMd | string | — | Current SKILL.md content (for refine mode) |
| existingTags | string[] | — | 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
/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
| Field | Type | Req | Description |
|---|---|---|---|
| url | string (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
/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:
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | Unique kebab-case identifier |
| name | string | Yes | Display name |
| version | string (semver) | Yes | Must match x.y.z format |
| domain | enum | Yes | work or personal |
| tags | string[] | Yes | Categorization tags |
| allowedTools | string[] | No | Tool allowlist |
| mcpServers | object | No | MCP server configurations |
| canUseToolPolicy | object | No | { autoApprove?: string[], autoDeny?: string[] } |
| maxTurns | number | No | Maximum agent conversation turns |
| supportedRuntimes | enum[] | No | Array of runtime IDs |
| preferredRuntime | enum | No | Preferred runtime for auto-routing |
| runtimeOverrides | object | No | Per-runtime instruction/tool/MCP overrides |
| capabilityOverrides | object | No | Per-runtime model, thinking, and server tool overrides |
| tests | object[] | No | { task: string, expectedKeywords: string[] }[] |
Supported Runtimes
| Runtime ID | Label |
|---|---|
| claude-code | Claude Code |
| openai-codex-app-server | OpenAI Codex App Server |
| anthropic-direct | Anthropic Direct API |
| openai-direct | OpenAI Direct API |
| ollama | Ollama (Local) |