Define Profiles

Create reusable MCP prompts with a role, suggested tools, and optional dynamic context.

Context Harness has two related but different concepts:

ConceptWho runs the model?What Context Harness providesUse it when
ProfileAn external client such as Cursor or your own applicationA reusable prompt, suggested tool set, and optional pre-resolved contextYou already have a chat or agent host and want a consistent project role
AgentContext HarnessA bounded model/tool loop with permissions, approvals, durable state, inspection, and recoveryYou want Context Harness to own and execute the task

A profile is not an autonomous agent and does not have a run lifecycle. It is a named recipe that prepares an external conversation. New configurations use profiles.*, the CLI uses ctx profile, and REST integrations use /profiles/*. The historical agent spellings remain deprecated aliases.

An executable agent is a standalone resource under .ctx/agents. See Build Local Agents for its complete setup.

You may not need a separate profile definition. Single-workspace ctx serve mcp projects each standalone .ctx/agents/*.toml resource as a stateless MCP prompt. That projection reuses the role and tool hints, while the external client—not the local runtime—owns model calls, tool execution, permissions, and history.

Why profiles?

Without a profile, every external conversation must repeat its role and context:

User: "You are a code reviewer. Search for our coding standards..."

With a profile, the workflow becomes:

User: "Review this PR" (using the code-reviewer profile)

The profile prepares a launch preset for the external client:

Profiles are useful for lightweight integration because they do not require Context Harness to own model credentials, conversation state, approvals, or the execution loop. For example, a Cursor extension can resolve a code-reviewer profile, apply its system prompt, enable search/get, and continue the conversation inside Cursor. Use an executable agent instead when the task must be durable, auditable, resumable, or governed by Context Harness runtime policy.

Profiles do not retain conversation history. The external client owns any conversation created from a profile.

Which one should I use?

Profile sources

ModeConfigBest for
Standalone projection.ctx/agents/<name>.tomlReusing one role for both managed runs and external conversations
Inline TOML[profiles.inline.<name>]Static reusable profiles
Lua script[profiles.script.<name>]Dynamic context injection, conditional logic
Rust traitimpl Profile for MyAgentCompiled extensions in custom binaries

Inline TOML profiles

The simplest way to define a profile—everything lives in ctx.toml:

[profiles.inline.code-reviewer]
description = "Reviews code changes against project conventions"
tools = ["search", "get"]
system_prompt = """
You are a senior code reviewer for this project. When reviewing code:
1. Use the `search` tool to find relevant coding conventions and patterns
2. Use the `get` tool to read full documents when snippets aren't enough
3. Be specific — cite which convention a suggestion relates to
4. Suggest improvements, not just problems

Always ground your feedback in the project's documented standards.
"""

[profiles.inline.architect]
description = "Answers architecture questions using indexed documentation"
tools = ["search", "get", "sources"]
system_prompt = """
You are a software architect with deep knowledge of this codebase.
Use the search tool to find architecture decision records (ADRs),
design documents, and relevant code patterns. Always cite your sources.
When recommending changes, explain tradeoffs clearly.
"""

These profiles appear immediately in GET /profiles/list and can be resolved via POST /profiles/{name}/prompt.


Lua scripted profiles

Use a scripted profile for dynamic context injection—for example, searching for relevant runbooks before an external conversation starts:

[profiles.script.incident-responder]
path = "profiles/incident-responder.lua"
timeout = 30
search_limit = 5
-- profiles/incident-responder.lua

profile = {}

profile.name = "incident-responder"
profile.description = "Helps triage production incidents with relevant runbooks"
profile.tools = { "search", "get", "create_jira_ticket" }

-- Arguments the user can provide
profile.arguments = {
    {
        name = "service",
        description = "The service experiencing the incident",
        required = false,
    },
    {
        name = "severity",
        description = "Incident severity (P1, P2, P3)",
        required = false,
    },
}

function profile.resolve(args, config, context)
    local service = args.service or "unknown"
    local severity = args.severity or "P2"

    -- Pre-search for relevant runbooks
    local results = context.search(
        service .. " incident runbook",
        { mode = "keyword", limit = config.search_limit or 5 }
    )

    -- Fetch full content and inject as context
    local runbook_text = ""
    for _, r in ipairs(results) do
        local doc = context.get(r.id)
        runbook_text = runbook_text .. "\n\n## " .. doc.title .. "\n" .. doc.body
    end

    return {
        system = string.format([[
You are an incident responder for the %s service (%s severity).
You have access to the following runbooks:
%s

Use the search tool for additional context.
Use create_jira_ticket when a tracking ticket is needed.
Be methodical: gather context, identify the issue, recommend actions.
        ]], service, severity, runbook_text),

        -- Inject a starter message
        messages = {
            {
                role = "assistant",
                content = string.format(
                    "I'm ready to help with the %s %s incident. "
                    .. "I've loaded %d relevant runbooks. What's the current situation?",
                    severity, service, #results
                ),
            },
        },
    }
end

return profile

The context bridge provides:

FunctionDescription
context.search(query, opts?)Search the knowledge base (keyword/semantic/hybrid)
context.get(id)Retrieve a full document by UUID
context.sources()List all data sources and their status
config argumentProfile-specific values from ctx.toml (env vars expanded)

HTTP endpoints

GET /profiles/list

Discover all registered profiles:

$ curl -s localhost:7331/profiles/list | jq '.profiles[] | {name, description, tools}'
{
  "name": "code-reviewer",
  "description": "Reviews code changes against project conventions",
  "tools": ["search", "get"]
}
{
  "name": "incident-responder",
  "description": "Helps triage production incidents with relevant runbooks",
  "tools": ["search", "get", "create_jira_ticket"]
}

POST /profiles/{name}/prompt

Resolve a profile’s prompt (for Lua profiles, this executes profile.resolve()):

$ curl -s localhost:7331/profiles/incident-responder/prompt \
    -H "Content-Type: application/json" \
    -d '{"service": "payments-api", "severity": "P1"}' | jq .
{
  "system": "You are an incident responder for the payments-api service (P1 severity)...",
  "tools": ["search", "get", "create_jira_ticket"],
  "messages": [
    {
      "role": "assistant",
      "content": "I'm ready to help with the P1 payments-api incident..."
    }
  ]
}
StatusMeaning
200Success
404Profile not found
500Lua resolve() failed
408Lua resolve() timed out

CLI commands

Profiles have their own ctx profile command group. These commands list, resolve, or scaffold profiles; they do not execute a model. ctx agent run, history, inspect, and resume belong to executable agents.

# List profiles, including projections from standalone executable agents
$ ctx profile list
  code-reviewer        Reviews code changes against project conventions   (tools: search, get)        [toml]
  architect            Answers architecture questions using indexed docs   (tools: search, get, sources) [toml]
  incident-responder   Helps triage production incidents with runbooks     (tools: search, get, create_jira_ticket) [lua]

# Resolve a Lua profile with arguments
$ ctx profile test incident-responder --arg service=payments-api --arg severity=P1

Profile: incident-responder
Source: lua (profiles/incident-responder.lua)
Tools: search, get, create_jira_ticket

System prompt (487 chars):
  You are an incident responder for the payments-api service (P1 severity).
  ...

Messages (1):
  [assistant] I'm ready to help with the P1 payments-api incident...

# Scaffold a new Lua profile
$ ctx profile init sre-helper
Created profile: profiles/sre-helper.lua
Add to config:

  [profiles.script.sre-helper]
  path = "profiles/sre-helper.lua"
  timeout = 30

Using profiles with Cursor

Once profiles are configured and the MCP server is running, Cursor or another client can resolve one when starting a conversation.

Step 1: Start the server with profiles:

$ ctx serve mcp --config ./config/ctx.toml
Registered 6 tools:
  POST /tools/search — Search indexed documents (builtin)
  POST /tools/get — Get document by ID (builtin)
  POST /tools/sources — List data sources (builtin)
Registered 3 profiles:
  POST /profiles/code-reviewer/prompt — Reviews code changes (toml)
  POST /profiles/architect/prompt — Answers architecture questions (toml)
  POST /profiles/incident-responder/prompt — Helps triage incidents (lua)
MCP server listening on http://127.0.0.1:7331

Step 2: Resolve a profile’s prompt and use it:

Call POST /profiles/{name}/prompt to get the system prompt, then use it in the external LLM conversation. The profile’s tools array tells the client which Context Harness tools are relevant; the client remains responsible for making them available and enforcing its own policy.

Step 3: In Cursor, the profile pattern works naturally:


SDLC profile examples

Here is a set of reusable profiles for external software-development conversations:

# config/ctx.toml

# ── Development ──────────────────────────────────
[profiles.inline.code-reviewer]
description = "Reviews code against project conventions and patterns"
tools = ["search", "get"]
system_prompt = """..."""

[profiles.inline.architect]
description = "Answers architecture questions using indexed ADRs and design docs"
tools = ["search", "get", "sources"]
system_prompt = """..."""

# ── Operations ───────────────────────────────────
[profiles.inline.sre-responder]
description = "Helps triage production incidents with runbooks and context"
tools = ["search", "get", "sources"]
system_prompt = """..."""

[profiles.inline.release-manager]
description = "Helps with release planning, changelogs, and deployment"
tools = ["search", "get", "sources"]
system_prompt = """..."""

# ── Knowledge ────────────────────────────────────
[profiles.inline.onboarding]
description = "Guides new engineers through the codebase"
tools = ["search", "get", "sources"]
system_prompt = """..."""

[profiles.inline.tech-writer]
description = "Writes documentation matching project style"
tools = ["search", "get"]
system_prompt = """..."""

# ── Domain Experts (Lua, dynamic) ────────────────
[profiles.script.domain-expert]
path = "profiles/domain-expert.lua"
timeout = 30
search_limit = 10

Custom Rust profiles

For compiled profiles in custom harness binaries, implement the Profile trait. The trait resolves a ProfilePrompt; it does not execute a model loop:

use context_harness::{Profile, ProfilePrompt, ProfileArgument};
use context_harness::traits::ToolContext;
use async_trait::async_trait;
use serde_json::Value;
use anyhow::Result;

pub struct DatabaseExpert;

#[async_trait]
impl Profile for DatabaseExpert {
    fn name(&self) -> &str { "db-expert" }
    fn description(&self) -> &str { "Database design and query optimization" }
    fn tools(&self) -> Vec<String> { vec!["search".into(), "get".into()] }

    fn arguments(&self) -> Vec<ProfileArgument> {
        vec![ProfileArgument {
            name: "database".into(),
            description: "Target database name".into(),
            required: false,
        }]
    }

    async fn resolve(&self, args: Value, ctx: &ToolContext) -> Result<ProfilePrompt> {
        let db = args["database"].as_str().unwrap_or("main");

        // Pre-search for schema documentation
        let results = ctx.search("database schema", None, None, None).await?;
        let context = results.iter()
            .map(|r| format!("- {}", r.title.as_deref().unwrap_or("?")))
            .collect::<Vec<_>>().join("\n");

        Ok(ProfilePrompt {
            system: format!(
                "You are a database expert for '{}'.\nRelevant docs:\n{}",
                db, context
            ),
            tools: self.tools(),
            messages: vec![],
        })
    }
}

Register it in your custom binary:

let mut profiles = ProfileRegistry::new();
profiles.register(Box::new(DatabaseExpert));
run_server_with_extensions(config, tools, Arc::new(profiles)).await?;

See the full example.


What’s next?