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
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.