Curiosity

Example Agents

A catalog of ready-to-adapt agents. Each entry pairs a use case with a full system prompt, a recommended tool set, a model class, and the call site that drives it. Copy the export block into Management → Agents → Import, swap in your own tool UIDs, and adjust the prompt to fit your domain.

For the underlying mechanics — variable substitution, structured output, the run lifecycle — see Creating Agents.

What agents are good for

Agents shine when a task has three properties:

  1. The shape is fixed, the input varies. "Triage this ticket", "summarise this transcript", "score this lead" — the procedure is the same every time; only the payload changes.
  2. A single LLM call isn't enough. The model needs to look something up, cross-reference a source, or chain a few decisions before answering.
  3. The output has to be machine-readable. A downstream endpoint, code index, or front-end component is going to consume the result, not just a human.

If only (1) holds, an AI tool is enough. If only (3) holds, an endpoint and a one-shot prompt template will do. Reach for an agent when the job has all three — that's when packaging prompt + tools + model + schema into one re-usable object pays for itself.

Typical jobs that fit:

Job Why an agent
Classify and tag incoming items Schema-typed result, often a small enum + free-text fields. Cheap model.
Triage with a suggested action Classification + one lookup tool + a fixed-shape decision.
Q&A with citations over your KB RAG pattern — search → fetch → answer with [1]-style refs.
Score a record (lead, account, risk) Numeric output bounded by a schema; can call enrichment tools.
Extract structured fields from prose Action items, attendees, key dates from a transcript; one big extraction.
Enrich every node of a type A code index loops over nodes; the agent generates derived properties.
Review a draft against policy Reads a policy document, judges the draft, returns issues with citations.

Example 1 — Ticket Triage

A classifier that picks a category, a severity, and a proposed next action for an incoming support ticket. No retrieval — pure prompting on the ticket body plus a small enum schema.

Ticket Triage agent export
[agent: Curiosity.Agents.Name("Ticket Triage")]
[agent: Curiosity.Agents.Description("Categorise a support ticket and propose the next action.")]
[agent: Curiosity.Agents.Icon("ticket")]
[agent: Curiosity.Agents.ChatTask("01HQ…Haiku")]     // small/fast model
[agent: Curiosity.Agents.OutputSchema("TriageDecision")]
// No tools — the model only needs the ticket body.

You triage incoming support tickets for ${PRODUCT}.

Read the ticket body and return a TriageDecision with:
  - Category: one of "Hardware", "Software", "Billing", "Account", "Other"
  - Severity: one of "Low", "Medium", "High", "Critical"
  - ProposedAction: a single sentence describing the next step
  - Reasoning: one or two sentences citing the words in the ticket that drove your call

Rules:
- Severity is "Critical" only when the ticket mentions data loss, security, or
  a production outage affecting more than one user.
- "Billing" beats "Account" when both seem to apply.
- Never invent customer details that aren't in the ticket body.

The schema:

[AgentOutputSchema("A categorised triage decision for a single support ticket.")]
public record TriageDecision(
    string Category,
    string Severity,
    string ProposedAction,
    string Reasoning);

Suggested tools: none. This is a pure-prompt classifier — adding search tools only encourages the model to wander.

Suggested model class: small / fast (Haiku-class). The decision is short and bounded.

Where to call it: from a webhook endpoint that fires on every new ticket; from a scheduled task that backfills triage decisions onto historical tickets; from a code index that materialises Category and Severity as derived properties (see Example 5).

Example 2 — Knowledge Base Q&A

A retrieval-augmented answerer. The agent searches the KB, fetches the top hits, and writes a grounded answer with [1]-style citations to the snippets it actually used.

KB Q&A agent export
[agent: Curiosity.Agents.Name("KB Question Answering")]
[agent: Curiosity.Agents.Description("Answer a customer question using only what's in the knowledge base, with citations.")]
[agent: Curiosity.Agents.Icon("book-open")]
[agent: Curiosity.Agents.ChatTask("01HQ…Sonnet")]
[agent: Curiosity.Agents.OutputSchema("GroundedAnswer")]
[agent: Curiosity.Agents.Tool("01J…SearchKB")]
[agent: Curiosity.Agents.Tool("01J…FetchArticle")]

You answer customer questions for ${PRODUCT} using only the knowledge-base
articles you can retrieve. Locale: ${LOCALE:-en}.

Procedure:
1. Call SearchKB with the user's question. Take the top 5 results.
2. For any result that looks promising, call FetchArticle to read the body.
3. Compose the answer using only sentences supported by the fetched articles.
   Each claim must reference a snippet id in brackets, e.g. [1].
4. If you cannot find evidence, return an answer with Confidence="None" and
   a one-sentence explanation of what's missing.

Never invent article ids. Never paraphrase content from an article you have
not actually fetched.
[AgentOutputSchema]
public record GroundedAnswer(
    string Answer,
    string Confidence,           // "High" | "Medium" | "Low" | "None"
    string[] CitedSnippetIds);   // matches the snippet ids registered by the tools

Suggested tools:

Tool What it does
SearchKB Hybrid + semantic search over KBArticle nodes via CreateSearchAsUserAsync. Returns scored snippets.
FetchArticle Pulls the full body of one article by UID via scope.ChatAI.GetTextFromNode. Registers a snippet for citation.

The full implementations of both tools live in Sub-agent Workflows → Workflow 1. Reuse them directly — they already thread scope.CurrentUser through, so the agent never sees articles the caller can't read.

Suggested model class: mid (Sonnet-class). Synthesis benefits from a larger context window; search hits routinely add up to 20 KB of input.

Where to call it: from a chat-front-end endpoint that takes a question and returns the GroundedAnswer; from a Slack bot via an external webhook; via the built-in POST /api/chatai/agents/run if the caller is already signed in.

Example 3 — Lead Qualifier

A scorer. Given an account UID, the agent pulls the account's graph context (recent activity, employee count, plan tier), assigns a numeric fit score, and explains the reasoning. The model never makes up numbers — it only quotes facts the tools returned.

Lead Qualifier agent export
[agent: Curiosity.Agents.Name("Lead Qualifier")]
[agent: Curiosity.Agents.Description("Score a sales lead against ICP fit using graph context. Returns a numeric score with reasons.")]
[agent: Curiosity.Agents.Icon("chart-line-up")]
[agent: Curiosity.Agents.ChatTask("01HQ…Sonnet")]
[agent: Curiosity.Agents.OutputSchema("LeadScore")]
[agent: Curiosity.Agents.Tool("01J…AccountSnapshot")]
[agent: Curiosity.Agents.Tool("01J…RecentActivity")]
[agent: Curiosity.Agents.Tool("01J…SimilarAccounts")]

You score sales leads for ${PRODUCT} against the Ideal Customer Profile:
  • Industry: SaaS, FinTech, Healthcare
  • Employee count: 50–5000
  • Has at least one prior demo in the last 90 days OR a champion contact

Procedure:
1. Call AccountSnapshot(accountUid) to get firmographic + plan info.
2. Call RecentActivity(accountUid, days=90) to see events and contacts.
3. Optionally call SimilarAccounts(accountUid) for peer benchmarks.
4. Score Fit from 0 to 100 using the rubric above. Reasons must quote
   specific values returned by the tools (e.g. "employee_count=320").

If a required tool returns no data, return Fit=0 and ScoreNotes explaining
what was missing.
[AgentOutputSchema]
public record LeadScore(
    int Fit,                  // 0..100
    string Tier,              // "A" | "B" | "C" | "D"
    string[] Reasons,         // 2–5 short bullets, each quoting a tool fact
    string ScoreNotes,        // free text, may be empty
    string[] CitedSnippetIds);

Suggested tools:

Tool What it returns
AccountSnapshot One JSON object per account UID: industry, employee_count, plan_tier, owner.
RecentActivity A list of events (demos, calls, emails) for the account over the last N days.
SimilarAccounts Up to 25 peer accounts via IQuery.ToSimilarity (see Similarity Engine).

Build each tool with scope.Graph.Q(scope.CurrentUser) so the salesperson never scores accounts they can't see.

Suggested model class: mid (Sonnet-class). Scoring needs sturdy reasoning over a moderate context.

Where to call it: from a /score-lead endpoint behind the CRM front-end; from a scheduled task that nightly refreshes scores for every open opportunity.

Example 4 — Meeting Minutes Extractor

An extraction agent. Given a raw transcript, it produces a structured summary, action items with owners and dates, decisions, and a list of attendees. One LLM call, one large structured output, no retrieval.

Meeting Minutes Extractor agent export
[agent: Curiosity.Agents.Name("Meeting Minutes")]
[agent: Curiosity.Agents.Description("Turn a raw meeting transcript into structured minutes with action items, decisions, and attendees.")]
[agent: Curiosity.Agents.Icon("clipboard-list")]
[agent: Curiosity.Agents.ChatTask("01HQ…Sonnet")]
[agent: Curiosity.Agents.OutputSchema("MeetingMinutes")]
// No tools — extraction is single-shot over the transcript.

You convert raw meeting transcripts into structured minutes.

Meeting metadata: title="${MEETING_TITLE}", date="${MEETING_DATE}".

Return a MeetingMinutes object containing:
  - Summary: 2–4 sentences describing what the meeting was about.
  - Attendees: full names mentioned as speaking or addressed by name. Skip
    interjections from people who only said hello.
  - ActionItems: each with Owner (full name), Action (imperative sentence),
    DueDate (ISO-8601 or empty if not stated).
  - Decisions: short statements of things the group agreed on.
  - OpenQuestions: items that were raised but not resolved.

Rules:
- Never invent an Owner. If an action is implied but no owner is named,
  leave Owner empty and put the implied person in ActionItems[].Action.
- Dates must be absolute (ISO-8601). Translate "next Friday" against
  ${MEETING_DATE}; if no anchor date is available, leave DueDate empty.
- Skip jokes, off-topic asides, and adjournment chatter.
[AgentOutputSchema]
public record MeetingMinutes(
    string Summary,
    string[] Attendees,
    ActionItem[] ActionItems,
    string[] Decisions,
    string[] OpenQuestions);

public record ActionItem(string Owner, string Action, string DueDate);

Suggested tools: none. Extraction over a single document works best as one model call.

Suggested model class: mid (Sonnet-class). Long transcripts can exceed 30 KB; a small model often drops attendees from the second half.

Where to call it: from an endpoint triggered when a transcript file is uploaded; from a scheduled task that walks Transcript nodes flagged as "unprocessed".

Example 5 — Document Enricher

An enricher that runs once per content node and returns a fixed bundle of derived properties: a one-paragraph summary, 3–6 topic tags, and a sentiment label. Designed to be called from a code index so that every Document in the workspace gets enriched automatically as it lands.

Document Enricher agent export
[agent: Curiosity.Agents.Name("Document Enricher")]
[agent: Curiosity.Agents.Description("Compute summary, tags, and sentiment for a single document. Deterministic schema.")]
[agent: Curiosity.Agents.Icon("wand-magic-sparkles")]
[agent: Curiosity.Agents.ChatTask("01HQ…Haiku")]    // small model — runs against every node
[agent: Curiosity.Agents.OutputSchema("DocumentEnrichment")]
// No tools — the document text is in the user message.

You enrich a single document for indexing.

You will receive the document's full text in the user message. Return a
DocumentEnrichment object with:
  - Summary: 1 paragraph, max 60 words, factual.
  - Tags: 3–6 lowercase noun phrases capturing the document's topics.
    Prefer concrete subjects ("battery firmware", "EU VAT") over generic
    ones ("technology", "process").
  - Sentiment: one of "Positive", "Negative", "Neutral", "Mixed".
  - Language: the ISO-639-1 code of the document's dominant language.

Constraints:
- Never quote more than 5 consecutive words from the input.
- Tags are nouns or noun phrases, not full sentences.
- If the document is empty or trivially short (< 40 words), return empty
  Tags and Sentiment="Neutral".
[AgentOutputSchema]
public record DocumentEnrichment(
    string Summary,
    string[] Tags,
    string Sentiment,
    string Language);

Suggested tools: none. Per-document enrichment is single-shot — adding tools forces the model into a slow tool-call loop you don't want at scale.

Suggested model class: small / fast (Haiku-class). A code index running this against 100 000 documents wants the cheap model.

Calling it from a code index

A custom code index is the natural place for per-node enrichment: the indexer hands you a batch of Document UIDs every time content changes, and your body decides what derived data to compute. The AgentAI accessor is available directly in the code-index scope — see Code Index Scope.

Document enrichment code index — body
// Targets: Document. Batch size: 200.

var failed = new List<UID128>();

foreach (var uid in ToIndex)
{
    if (CancellationToken.IsCancellationRequested) return ToIndex;

    var text = ChatAI.GetTextFromNode(uid, limit: 12_000);
    if (string.IsNullOrWhiteSpace(text)) continue;       // empty / deleted between queue and run

    try
    {
        var run = await AgentAI.RunAgentAsync(
            agentUID:          AI_Agents.Document_Enricher,
            userMessage:       text,
            userUID:           default,                  // system-scoped indexer
            cancellationToken: CancellationToken);

        if (run.Status != AgentRunStatus.Completed)
        {
            failed.Add(uid);
            continue;
        }

        // run.Result is the merged surfaced text of the run. smartParsing
        // strips any ```json … ``` fence the model wrapped its output in.
        var enriched = run.Result?.FromJson<DocumentEnrichment>(smartParsing: true);

        if (enriched is null) { failed.Add(uid); continue; }

        // Write the derived properties back onto the same node.
        var locked = await Graph.TryGetLockedAsync(uid);
        if (locked is null) { failed.Add(uid); continue; }

        try
        {
            locked.SetString("Summary",   enriched.Summary);
            locked.SetString("Sentiment", enriched.Sentiment);
            locked.SetString("Language",  enriched.Language);
            locked.SetStrings("Tags",     enriched.Tags ?? Array.Empty<string>());
            await Graph.CommitAsync(locked);
        }
        catch
        {
            await Graph.AbandonAsync(locked);
            failed.Add(uid);
        }
    }
    catch
    {
        failed.Add(uid);
    }
}

return failed;

Notes on the shape:

  • AgentAI is available in code-index scope. Same accessor (Mosaik.GraphDB.Safe.AgentAI) as on ToolScope, so the call returns a resolved AgentRun object — no second graph round-trip.
  • userUID: default runs the agent under a system identity. The indexer is not user-facing; the per-user ACL filter happens later when someone searches or reads the enriched properties. If your agent calls tools, pass a real user UID instead — see the warning below.
  • run.Result is the merged surfaced text of the run (sequential Text parts joined in order; thinking and tool-call frames are excluded). For the granular trace — including thinking and tool invocations — use run.ResultParts.
  • smartParsing: true on FromJson strips any Markdown fence (e.g. ```json … ```) the model wrapped its output in. Cheaper than fighting the prompt to suppress fences.
  • Failed UIDs are returned so the indexer can requeue them with backoff. A transient LLM hiccup doesn't lose work.
  • CancellationToken is honoured at the top of every iteration, the way every code-index body must — see Creating a Code Index → Cancellation handling.

Pick the batch size against the LLM, not the graph. A 100 000-item batch that calls a remote LLM per UID will spend most of its time blocked on HTTP. Set Maximum Batch Size to a few hundred so the indexer can interrupt cleanly, and use a small / fast model class.

Don't pass userUID: default if the agent's tool set has unscoped admin tools. The system identity bypasses some ACL filters. If your enricher is pure-prompt (no tools) this is fine; otherwise, run the indexer under a dedicated service user and grant it only the permissions the enrichment needs.

Picking the right shape

A quick map from the job you have to the example above:

You want to… Start from Then change…
Classify a single record Ticket Triage The enum values in the schema and the prompt rules.
Answer a question over your KB KB Q&A The node type the search tool targets.
Score / rank a record Lead Qualifier The rubric and the firmographic tools.
Extract structured fields from prose Meeting Minutes The schema and the field rules.
Enrich every node of a type Document Enricher The schema and the target node properties.

The two patterns to internalise:

  • Single-shot extractors (Triage, Minutes, Enricher) — no tools, one model call, one structured output. Small model, big batch.
  • RAG agents (KB Q&A, Lead Qualifier) — 2–4 focused tools, one larger model, a schema with explicit citation fields. Mid-tier model.

For multi-agent compositions (planner + specialists), see Sub-agent Workflows.

See also

© 2026 Curiosity. All rights reserved.