Curiosity

Custom Endpoints

Custom endpoints are the primary extension surface for Curiosity Workspace. They are server-side C# functions that run inside the workspace process and have direct access to the graph, search, AI runtime, and the calling user's identity.

Typical uses:

  • Permission-aware retrieval that combines graph traversal with text/vector search.
  • Domain-specific analytics (counts, aggregates, neighbor expansions).
  • AI orchestration (retrieve → ground → generate → store).
  • A stable, versioned HTTP API for external systems.
  • Action endpoints that mutate the graph as the calling user.

Curiosity Workspace Custom Endpoints List

Anatomy of an endpoint

An endpoint is C# code authored from Settings → Custom Endpoints → Create endpoint. The workspace compiles, hot-deploys, and exposes it at:

POST /api/endpoints/run/<endpoint-name>            (session JWT or scoped API token)
POST /api/endpoints/token/run/<endpoint-name>      (endpoint token)

Inside the endpoint you have:

Symbol Purpose
Body.FromJson<T>() Parse the JSON request body into a typed shape.
CurrentUser The authenticated user (null for system-token calls).
CancellationToken Propagates client cancellation.
Graph The graph engine — schemas, upserts, traversals.
Q() Shorthand for Graph.Query(); the fluent traversal chain.
Graph.CreateSearchAsUserAsync(req, CurrentUser, ct) Permission-aware search.
Graph.CreateSearchAsync(req) System-context search (ignores ACLs — admin use only).
Logger Workspace-level structured logger.

The endpoint's return value is serialized to JSON and returned to the caller. Returning BadRequest("…"), NotFound("…"), etc., produces the corresponding HTTP status.

Hello-world endpoint

The smallest useful endpoint — echoes back the calling user and a message:

class EchoRequest { public string Message { get; set; } }

var req = Body.FromJson<EchoRequest>();
if (string.IsNullOrWhiteSpace(req?.Message))
    return BadRequest("message is required");

return new {
    callerUid  = CurrentUser?.UID,
    callerName = CurrentUser?.GetString("Name"),
    receivedAt = DateTimeOffset.UtcNow,
    echo       = req.Message,
};

Call it:

curl -X POST "http://localhost:8080/api/endpoints/run/echo" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"message":"hi"}'

Permission-aware retrieval endpoint

This is the shape you'll write most often. Combines a search request scoped to a node type with a graph-derived target set, executed in the user's security context:

class SimilarTicketsRequest
{
    public string Query        { get; set; }
    public string ProductSku   { get; set; }
    public int    Limit        { get; set; } = 10;
}

var req = Body.FromJson<SimilarTicketsRequest>();
if (string.IsNullOrWhiteSpace(req.Query))
    return BadRequest("query is required");

var search = SearchRequest.For(req.Query);
search.BeforeTypesFacet = new([] { nameof(Ticket) });

if (!string.IsNullOrWhiteSpace(req.ProductSku))
{
    // graph-scoped: only tickets ForProduct = this product
    search.TargetUIDs = Q().StartAt(nameof(Product), req.ProductSku)
                           .In(Edges.ForProduct)
                           .AsUIDEnumerable()
                           .ToArray();
}

// Permission-aware: filters by CurrentUser's team memberships
var query = await Graph.CreateSearchAsUserAsync(search, CurrentUser, CancellationToken);
return query.Take(Math.Min(req.Limit, 50)).Emit("N");

For more retrieval patterns, see Custom Queries.

RAG endpoint with citations

Wraps retrieval, prompt construction, and an LLM call so the chat UI (or any external caller) gets a grounded answer plus a citation map:

class RagRequest { public string Question { get; set; } }
var req = Body.FromJson<RagRequest>();

// 1. Retrieve (permission-aware)
var search = SearchRequest.For(req.Question);
search.BeforeTypesFacet = new([] { nameof(Ticket) });
var hits = (await Graph.CreateSearchAsUserAsync(search, CurrentUser, CancellationToken))
            .Take(8).AsEnumerable().ToArray();

// 2. Build a grounded context
var snippets = hits.Select((n, i) => new {
    id      = i + 1,
    uid     = n.UID,
    subject = n.GetString(nameof(Ticket.Subject)),
    body    = Graph.GetIndexedText(n.UID, limit: 4_000),
}).ToArray();

var prompt = $@"Answer the user's question using ONLY the snippets below.
Cite snippets with bracket IDs like [1], [2].
If the answer isn't in the snippets, say you don't know.

Question: {req.Question}

Snippets:
" + string.Join("\n\n", snippets.Select(s =>
    $"[{s.id}] {s.subject}\n{s.body}"));

// 3. Generate
var answer = await Graph.CallChatModelAsync(prompt, CancellationToken);

return new { answer, citations = snippets };

For the full RAG architecture (and why this shape), see RAG and agent architecture.

Action endpoint with input validation

Endpoints that mutate state should validate aggressively and require explicit permission checks:

class AssignRequest { public string TicketId { get; set; } public string AssigneeId { get; set; } }
var req = Body.FromJson<AssignRequest>();

if (string.IsNullOrWhiteSpace(req.TicketId) || string.IsNullOrWhiteSpace(req.AssigneeId))
    return BadRequest("ticketId and assigneeId are required");

if (CurrentUser is null)
    return Unauthorized("authentication required");

var ticket = Node.FromKey(nameof(Ticket), req.TicketId);
if (!await Graph.CanUserSeeAsync(ticket, CurrentUser))
    return NotFound();  // don't reveal whether the ticket exists

var assignee = Node.FromKey("_User", req.AssigneeId);
Graph.Link(ticket, assignee, "AssignedTo");
await Graph.CommitPendingAsync();

Logger.LogInformation("ticket {Ticket} assigned to {Assignee} by {User}",
    req.TicketId, req.AssigneeId, CurrentUser.UID);
return Ok();

Notes:

  • Return NotFound rather than Forbidden for resources the user can't see — it avoids leaking existence.
  • Log mutations with the caller and target IDs; the audit log captures these.
  • Use CommitPendingAsync() to flush; until then the write is queued.

Testing endpoints

There are three good places to validate endpoint behaviour:

  1. In the endpoint editor — the workspace UI includes an inline JSON request console you can use to call the endpoint as yourself or as a different user.
  2. From the admin Shell — call helper methods to test slices of your endpoint code without going through HTTP.
  3. From CI — use the REST API with a scoped API token. Add tests that include negative cases (missing fields, malformed JSON, insufficient scope, unauthorized user).

Versioning, promotion, and rollback

  • Save in the editor — the new version compiles in the background. Until it succeeds, the previous version stays live.
  • For breaking changes, name the new endpoint with a suffix (similar-tickets-v2) and keep the old one live during the deprecation window.
  • Export endpoint source to git, treat it as code, promote through dev → staging → prod via your CI. See Deployment.
  • To roll back, revert the endpoint source in the editor (the editor keeps a per-save history).

Security checklist

  • Validate every input from Body.FromJson<T>().
  • Use CreateSearchAsUserAsync(..., CurrentUser, ...) for user-facing retrieval, not the system variant.
  • Return safe errors with a traceId (the workspace adds this automatically).
  • Avoid SSRF: allowlist destinations if your endpoint makes outbound HTTP calls.
  • Set request/response size limits — large responses degrade chat latency and inflate logs.
  • Never embed secrets in endpoint source; read them from workspace settings or env.
  • Long-running work: honor CancellationToken.

See Security.

When not to write an endpoint

  • For ingestion, use a data connector with Curiosity.Library instead. See Connectors.
  • For periodic work, use a scheduled task. See Scheduled Tasks.
  • For an action that should be reachable from chat, also (or only) expose an AI tool. See AI Tools.
  • For graph queries you're going to call once during exploration, use the admin Shell — don't promote ad-hoc queries into endpoints.

Common endpoint shapes

  • Query endpoints: graph traversal returning nodes, neighbors, or projections.
  • Search endpoints: wrap SearchRequest with workspace defaults, type scope, and graph constraints.
  • Similarity endpoints: combine embeddings (StartAtSimilarText) with graph filters and a stable response shape.
  • RAG endpoints: retrieve → format → call LLM → return answer + citations.
  • Action endpoints: validate → check permissions → mutate → audit.

Next steps

© 2026 Curiosity. All rights reserved.
Powered by Neko