Curiosity

Endpoint Execution Scopes

When writing Custom Endpoints, your code executes within a specific scope that determines what operations are allowed.

Endpoints Scope

This is the standard scope for endpoints running on a Primary node or when the system allows write access.

Class: CodeEndpointExecutionScope

Property/Method Type Description
Graph / G Mosaik.GraphDB.Safe.Graph Thread-safe graph for reading and writing.
Query() / Q() IQuery Creates a new query.
Logger ILogger Logger for the scope.
Body string The HTTP request body.
Headers IReadOnlyDictionary Request headers.
CurrentUser UID128 The UID of the authenticated user. Will be default when the endpoint is invoked via an authorization token rather than a user session.
CancellationToken CancellationToken Request cancellation token.
ChatAI ChatAI Chat AI helper.
Tracker QueryTracker Performance tracker.
RelayStatusAsync(msg) Task Send a status update string to the caller.
StatusCode(statusCode, content, contentType) EndpointResponse Return a response with an explicit HTTP status code.
Ok(content, contentType) EndpointResponse Return a 200 OK response.
BadRequest(content, contentType) EndpointResponse Return a 400 Bad Request response.
Unauthorized(content, contentType) EndpointResponse Return a 401 Unauthorized response.
NotFound(content, contentType) EndpointResponse Return a 404 Not Found response.
Forbid(content, contentType) EndpointResponse Return a 403 Forbidden response.
Redirect(content, contentType) EndpointResponse Return a 302 Redirect response.
RunEndpointAsync<T> Task<T> Execute another endpoint in-process. By default inherits the current user and admin status; pass user or runAsAdmin to override.
RunEndpointOnPrimaryAsync<T> Task<T> Execute an endpoint on the Primary node. From the primary, falls through to RunEndpointAsync. From a replica, forwards over HTTP.
RunToolAsync Task<ToolCallResult> Execute an AI tool by UID and function name.
GetDownloadTokenForFile(fileUID, validity) string Generate a time-limited download token for a file node.

Read-Only Endpoints Scope

This scope is used when an endpoint is executed on a Read-Only Replica, or when the endpoint itself is marked ReadOnly (the [endpoint: Curiosity.Endpoints.ReadOnly] attribute on export). You cannot modify the graph directly in this scope. However, you can use RunEndpointOnPrimaryAsync to forward write operations to the primary node.

Class: ReadOnlyCodeEndpointExecutionScope

Property/Method Type Description
Graph / G ReadOnlyGraph Read-only graph access.
Query() / Q() IQuery Creates a new query.
Logger ILogger Logger for the scope.
Body string HTTP request body.
CurrentUser UID128 The UID of the authenticated user. Will be default when the endpoint is invoked via an authorization token.
CancellationToken CancellationToken Request cancellation token.
ChatAI ChatAI Chat AI helper.
Tracker QueryTracker Performance tracker.
RelayStatusAsync(msg) Task Send a status update string to the caller.
StatusCode(statusCode, content, contentType) EndpointResponse Return a response with an explicit HTTP status code.
Ok(content, contentType) EndpointResponse Return a 200 OK response.
BadRequest(content, contentType) EndpointResponse Return a 400 Bad Request response.
Unauthorized(content, contentType) EndpointResponse Return a 401 Unauthorized response.
NotFound(content, contentType) EndpointResponse Return a 404 Not Found response.
Forbid(content, contentType) EndpointResponse Return a 403 Forbidden response.
Redirect(content, contentType) EndpointResponse Return a 302 Redirect response.
RunEndpointAsync<T> Task<T> Execute another endpoint in-process.
RunEndpointOnPrimaryAsync<T> Task<T> Forward execution to the Primary node for write operations.
RunToolAsync Task<ToolCallResult> Execute an AI tool by UID and function name.
GetDownloadTokenForFile(fileUID, validity) string Generate a time-limited download token for a file node.

RunEndpointAsync<T> — calling another endpoint in-process

RunEndpointAsync invokes another endpoint inside the same workspace process — no HTTP, no loopback. The target endpoint's Code is compiled (or fetched from the compile cache) and its scope is constructed with the body, headers, user, and admin status you pass in.

Full signature:

Task<T> RunEndpointAsync<T>(
    EndpointUID endpointName,
    string body = null,
    UID128? user = null,
    bool? runAsAdmin = null,
    bool propagateStatus = true,
    IReadOnlyDictionary<string, StringValues> headers = null,
    CancellationToken cancellationToken = default
) where T : class;

Behavior:

Argument Default Effect
endpointName required Use the auto-generated Endpoints.Group_Path constant for compile-time safety.
body null Passed as-is to the target. The target reads it via Body.FromJson<T>().
user inherits CurrentUser Pass another UID128 to call as that user; pass default(UID128) to call as the token caller (skips per-user ACL filtering).
runAsAdmin inherits caller's admin state Pass true to elevate, false to drop admin even if the caller has it.
propagateStatus true Forwards RelayStatusAsync updates from the child up to the original HTTP caller.
headers null Optional per-call header overrides. The target sees these in its Headers property.
cancellationToken linked to scope's CT Cancelling the parent endpoint cancels the child.

T is deserialized from the response body. Use string to receive the raw payload; use a record/POCO to JSON-deserialize.

// Identity preserved — the called endpoint sees CurrentUser, same admin status.
var summary = await RunEndpointAsync<CaseSummary>(
    Endpoints.Cases_Summarize, $"{{\"caseId\":\"{caseId}\"}}");

// Run as a different user (e.g. the case owner) with a fresh admin state.
var report = await RunEndpointAsync<string>(
    Endpoints.Cases_ExportPdf, body: null, user: caseOwnerUID, runAsAdmin: false);
"When to use HTTP instead"

RunEndpointAsync is for server-side composition — one endpoint orchestrating another. From a front-end, connector, or external client, use the HTTP routes documented in Calling endpoints.

RunEndpointOnPrimaryAsync<T> — from a read-only replica

In a primary / read-only replica deployment, replicas serve reads from the same shared storage as the primary but cannot write. RunEndpointOnPrimaryAsync is the escape hatch when a read-only endpoint needs to perform writes — it forwards the call to the primary so the work happens there.

Full signature (on the writable scope):

Task<T> RunEndpointOnPrimaryAsync<T>(
    EndpointUID endpointName,
    string body = null,
    UID128? user = null,
    bool? runAsAdmin = null,
    bool propagateStatus = true,
    CancellationToken cancellationToken = default
) where T : class;

How it behaves depends on which node you're on:

Node Behavior
Primary Falls through to RunEndpointAsync<T>(...) — same in-process call, no network hop. Same code path on either node.
Read-only replica Opens an HTTP client to MSK_PRIMARY_ADDRESS, attaches a bearer token plus the X-MSK-User header with the caller's UID, and POSTs body (JSON) to {primary}/api/endpoints/from-replica/run/{path}.

Flow from a replica

sequenceDiagram participant Client participant Replica as Read-Only Replica participant Primary Client->>Replica: POST /api/endpoints/.../my-readonly-endpoint Replica->>Replica: Run read-only endpoint code Replica->>Primary: POST /api/endpoints/from-replica/run/my-write-endpoint Note over Primary: Executes RunEndpointAsync<br/>as the original user loop While 202 Accepted Primary-->>Replica: 202 + CalculationProgress header Replica-->>Client: relays status (RelayStatusAsync) Replica->>Primary: poll (1s) end Primary-->>Replica: 200 + body Replica-->>Client: 200 + body

The replica polls every second while the primary returns 202 Accepted (used by pooling endpoints). The CalculationProgress header, when present, is forwarded through RelayStatusAsync so the original caller sees the same status messages they would see if the work had run locally.

Identity propagation

The user UID travels in the X-MSK-User header. On the primary, CurrentUser resolves to that UID, and ACL filtering applies as usual. If CurrentUser was default on the replica (token call), it stays default on the primary.

"Headers are not forwarded"

The HTTP path does not currently replicate the request headers dictionary — only the user UID is propagated. If your write endpoint reads Headers["X-Custom"], that header will be empty when called via the replica path. Pass anything you need through the body instead.

Typical pattern

public record AddNoteRequest(string CaseId, string Note);

if (Graph.Internals.ReadOnly)
{
    return await RunEndpointOnPrimaryAsync<string>(Endpoints.Cases_AddNote, Body);
}

// Running on the primary — do the write locally.
var req = Body.FromJson<AddNoteRequest>();
var node = await Graph.GetOrAddLockedAsync(N.SupportCase.Type, req.CaseId);
node.Set(N.SupportCase.Notes, req.Note);
await Graph.CommitAsync(node);
return "ok";

For the full replica configuration (storage requirements, env vars), see Read-Only Replicas.

RunToolAsync — invoking an AI tool

Task<ToolCallResult> RunToolAsync<T>(
    UID128 toolUID,
    string functionName,
    string argumentsJson = null,
    UID128? user = null,
    UID128 chatTaskUID = default,
    CancellationToken cancellationToken = default
) where T : class;

The generic T is currently unused; the return is always ToolCallResult. Tools always run as the supplied (or current) user with ACL enforcement — there is no admin override here.

var result = await RunToolAsync<string>(
    AI_Tools.SearchKnowledgeBase,
    functionName: "search",
    argumentsJson: $"{{\"query\":\"laptop overheating\",\"top_k\":5}}");

return Ok(result.Output);

Use the auto-generated AI_Tools.* constants from Auto-generated Helpers instead of hard-coding UIDs.

Sharing code between endpoints and AI tools

To call helper methods defined in another endpoint or share types across endpoints and AI tools, use the //ImportEndpoint("path") directive — see Importing Endpoint Code.

For more on Replicas, see Read-Only Replicas.

© 2026 Curiosity. All rights reserved.