Endpoint Execution Scopes

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

Reading the request body

Body is the request stream, not a string, so a payload is parsed straight off it instead of being turned into text first:

public record AssignRequest(UID128 CaseUID, UID128 AssigneeUID);

var req = ParseBody<AssignRequest>();
  • ParseBody<T>() reads the body as JSON. An empty body gives back the default value. The stream is rewound for you, so it can be called more than once.
  • ParseBody<UID128>() is the exception: it reads a bare, optionally JSON-quoted UID rather than JSON — the endpoint shape whose whole payload is a node identifier.
  • ReadBodyAsText() reads the whole body as text, for a payload that is not JSON. It returns an empty string when there is no body.
Migrating older endpoint code

Body used to be a string, read with Body.FromJson<T>() or UID128.Parse(Body.Trim('"')). Both spellings are rewritten to ParseBody<T>() automatically by a migration that runs once on upgrade, and each rewrite is recorded in the endpoint's code history so it can be reviewed or rolled back. Code you paste in by hand has to use the new spelling — the old one no longer compiles.

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 Stream The request body, as the stream it arrived on. Read it with ParseBody<T>() or ReadBodyAsText() rather than by hand.
ParseBody<T>() T Read the body as JSON and return it as T. An empty body gives back the default value. ParseBody<UID128>() is the exception and reads a bare (optionally JSON-quoted) UID rather than JSON.
ReadBodyAsText() string Read the whole body as text, for a payload that is not JSON. Returns an empty string when there is no body.
CreateHttpClient() HttpClient A pooled HttpClient for the endpoint's outbound calls. Prefer it over new HttpClient().
RunTaskAsync(childScope => …) Task / Task<T> Run background work against a scope of its own. This replaces Task.Run, which does not compile here — see Async operations.
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. Only the latest message is kept — each call replaces the previous one. The message is relayed through the CalculationProgress HTTP header, so it is truncated to 200 characters and any character invalid in a header (line breaks, other control characters, non-ASCII) is replaced with a space.
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 Stream The request body, as the stream it arrived on.
ParseBody<T>() T Read the body as JSON and return it as T.
ReadBodyAsText() string Read the whole body as text, for a payload that is not JSON.
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. Only the latest message is kept — each call replaces the previous one. The message is relayed through the CalculationProgress HTTP header, so it is truncated to 200 characters and any character invalid in a header (line breaks, other control characters, non-ASCII) is replaced with a space.
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.* constant for compile-time safety. Members are nested by path: an endpoint at cases/summarize is Endpoints.Cases.Summarize.
body null Passed as-is to the target. The target reads it via ParseBody<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,   // endpoint at path "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,   // endpoint at path "cases/export-pdf"
    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); // endpoint at path "cases/add-note"
}

// Running on the primary — do the write locally.
var req = ParseBody<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, whose Result property holds the tool's string output. 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.Result);

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.