Creating Endpoints

A custom endpoint is C# code that runs inside the workspace process and is reachable over HTTP. You author and deploy them through the workspace UI — there's no separate build step.

For the full hands-on walkthrough see Custom endpoint from scratch. This page is the reference for the dialog itself.

Create

  1. Open Management → Endpoints.
  2. Click + New endpoint.
  3. Fill in the configuration (below).
  4. Paste your code and Save. The workspace compiles and hot-loads.

Curiosity Workspace Custom Endpoints

Configuration

Setting Values What it does
Endpoint path hello-world, kb/answer URL segment. Use slashes for hierarchy.
Mode Sync / Pooling Sync holds the HTTP connection. Pooling returns 202 Accepted and lets the client poll.
Authorization Unrestricted / Restricted Unrestricted endpoints are reachable without a token. Restricted requires a user session or an endpoint token.
Read only true / false When true, the endpoint can run on a read-only replica; the runtime blocks writes.
Run as admin true / false Bypasses the caller's ACL filtering. Use sparingly and never for endpoints called by end users.
Mode picker
Sub-second response, no LLM work Sync
Calls ChatAI.CompleteAsync on a large prompt Pooling
Periodic batch run with no caller Use a scheduled task instead.

Code shape

The body of an endpoint runs inside an *ExecutionScope — the type depends on whether the endpoint is marked read-only. Every member documented in Endpoint execution scopes is available as a top-level identifier.

Minimal sync endpoint:

return $"Hello! The current time is {DateTimeOffset.UtcNow:u}";

Typed JSON request and response:

public record EchoRequest (string Message);
public record EchoResponse(string Message, DateTimeOffset At);

var req = ParseBody<EchoRequest>();
return Ok(new EchoResponse(req.Message, DateTimeOffset.UtcNow));

Returning an HTTP error explicitly:

if (string.IsNullOrWhiteSpace(req.Message))
    return BadRequest("Message is required.");

How callers reach it

URL When
{workspace}/api/endpoints/external/{path} Unrestricted endpoints (public).
{workspace}/api/endpoints/token/run/{path} Restricted endpoints (Bearer token).
await Mosaik.API.Endpoints.CallAsync<T>(path, body?) From a Tesserae front-end (session-authenticated).
await client.CallAsync<T>(path, body) From a connector or external service (EndpointsClient).

See Calling endpoints for the full request shapes, including pooling.

Pooling mode

When the endpoint may run longer than the proxy / browser allows on a single connection, switch to Pooling:

  • The first call returns 202 Accepted with an MSK-ENDPOINT-KEY header.
  • The caller polls the same URL (passing the key) until it gets 200 OK.
  • RelayStatusAsync(string) from the endpoint streams status updates to the caller. Messages are capped at 200 characters and non-header-safe characters (line breaks, control characters, non-ASCII) are replaced with a space before being sent — keep status text to a short, single-sentence summary.

The built-in EndpointsClient and Mosaik.API.Endpoints.CallAsync handle the polling transparently. From curl / external clients see Calling endpoints.

Read-only endpoints

Marking an endpoint read only has two effects:

  1. The endpoint can run on a read-only replica (useful for scaling read-heavy workloads).
  2. The runtime swaps CodeEndpointExecutionScope for ReadOnlyCodeEndpointExecutionScope, which exposes a ReadOnlyGraph and removes the write methods.

If you need to write from a read-only endpoint, call RunEndpointOnPrimaryAsync<T>(path, body) to forward to the primary node.

Versioning and promotion

Endpoints live inside the workspace graph as nodes. Treat them like deployable code:

  1. Export the endpoint definition from the workspace UI (Management → Endpoints → ⋯ → Export).
  2. Commit the exported file to git.
  3. Re-import on promotion to staging / production.

See the production deployment checklist for the surrounding flow.

Renaming an endpoint

An endpoint's node is addressed by a hash of its path, so changing the path is not an edit — it produces a new node, and the old one keeps answering on the old route. Every other kind of definition (AI tool, agent, prompt template, task) is identified by a UID that survives a rename, which is why endpoints have a marker of their own:

[endpoint: Curiosity.Endpoints.Path("reports/monthly-summary")]
[endpoint: Curiosity.Endpoints.PreviousPath("reports/monthly")]

PreviousPath is stored on the node and written into the export only when it is set, so an endpoint nobody renamed exports exactly as it did before. A workspace that performed a rename re-exports the marker, which carries the rename onward to the next workspace the configuration is imported into.

In the admin editor the marker is recorded for you: saving an endpoint under a new path deletes the old node, creates the new one, and stores the old route as its previous path. A save that is not a rename passes the recorded previous path straight back, so an earlier rename is not erased before an export has carried it anywhere.

On import, renames are applied in a second pass, after every endpoint in the bundle has been written — whether the old route may be removed depends on what else the same import carried. Three things it never removes:

  • A route the same import also wrote. An endpoint that took the old route over is a different endpoint, so swapping two routes, or handing an old route to another endpoint in the same bundle, does what it says.
  • A UID holding something that is not a code endpoint. That is reported as an error and left alone rather than deleting an unrelated part of the workspace.
  • The renamed endpoint itself. The UID is a case-insensitive hash of the path, so "renaming" Foo to foo is the same node.

Naming a previous path that no longer exists is not an error — the same bundle has to be safe to import twice.

In the admin assistant's shell a rename is three things in one commit: change Path to the new route, mv the file so its name matches (that is what removes the old endpoint from this workspace), and add the PreviousPath attribute (that is what carries the rename to every other workspace). Doing only two of the three leaves either the old route serving traffic elsewhere or a file whose name and path disagree, which build refuses.

Anything still calling the old route gets a 404 once the rename is applied, so move the callers — a front-end view, a connector, a scheduled task, another endpoint's //ImportEndpoint — in the same change. See Configuration sync for how the bundle is produced and imported.

© 2026 Curiosity. All rights reserved.