Curiosity

Importing Endpoint Code in Other Endpoints and AI Tools

The single supported way to share C# code across custom endpoints and AI tools is the //ImportEndpoint("path") directive. The workspace recognizes this line-comment, looks up the referenced endpoint, and appends its source to the end of the importing script before compiling. The directive itself stays in place as a comment, so source-line numbers in error messages still point at your code.

This page covers how the mechanism works, how to use it for shared helpers, and the rules around it (cycles, ordering, ACLs).

The directive

A single line at the top (or anywhere) of an endpoint or AI tool's code:

//ImportEndpoint("path/to/another-endpoint")
  • The path is the endpoint path of the source — the same string you'd put in a URL.
  • It is matched by a case-insensitive regex; whitespace inside the parentheses is allowed.
  • Use the auto-generated Endpoints.* constants where you can — see below.

When the script is compiled, every imported endpoint's Code is concatenated onto the end of the importer. Imports are recursive (an imported endpoint can //ImportEndpoint(...) further endpoints) and each unique endpoint is appended only once. A cyclic import (A imports B, B imports A) throws InvalidOperationException("Cyclic endpoint import detected: ...") at compile time.

The graph also tracks the dependency: each //ImportEndpoint produces an _Imported edge from the importer to the import, and an _ImportedBy edge in the reverse direction. You can browse these in the workspace UI to see which endpoints depend on which.

A shared helper endpoint

Put reusable types and methods in their own endpoint. Restrict it so it can't be called over HTTP — typically by setting Authorization to Restricted and not issuing tokens for it, or by including a guard at the top.

Endpoint path: _lib/case-helpers

// Shared helpers for support case endpoints. Not meant to be invoked directly.
return Forbid("This endpoint is a shared library, not callable directly.");

#pragma warning disable CS0162 // Unreachable code — only the import-side uses the helpers below.

public static class CaseHelpers
{
    public record CaseDto(string Id, string Title, string Status, DateTimeOffset Updated);

    public static CaseDto ToDto(INode node) => new(
        node.GetKey(),
        node.Get<string>(N.SupportCase.Title) ?? "",
        node.Get<string>(N.SupportCase.Status) ?? "Unknown",
        node.Get<DateTimeOffset?>(N.SupportCase.Updated) ?? DateTimeOffset.MinValue);

    public static bool IsHighPriority(INode node)
        => node.Get<int>(N.SupportCase.Priority) >= 8;
}

The early return ensures direct HTTP calls fail closed. The #pragma warning disable quiets the "unreachable code" warning the compiler raises for the helper definitions below.

Consuming the helpers

Two consumer endpoints share the same types via a single import each:

Endpoint path: cases/get

//ImportEndpoint("_lib/case-helpers")

var id = Body.FromJson<string>();
if (!Graph.TryGetReadOnlyContent<SupportCase>(N.SupportCase.Type, id, out var node))
    return NotFound($"Case '{id}' not found.");

return Ok(CaseHelpers.ToDto(node).ToJson());

Endpoint path: cases/list-high-priority

//ImportEndpoint("_lib/case-helpers")

var hits = Q().StartAt(N.SupportCase.Type)
              .Where(N.SupportCase.Status, "Open")
              .AsEnumerable()
              .Where(CaseHelpers.IsHighPriority)
              .Select(CaseHelpers.ToDto)
              .ToList();

return Ok(hits.ToJson());

Both endpoints now share the CaseDto shape and the IsHighPriority rule — change the helper in one place and both consumers pick up the change after recompilation.

Using Endpoints.* constants

The Auto-generated Helpers include an Endpoints static class whose constants hold endpoint paths. Prefer them — they survive renames and refactors.

The directive itself is a comment, so it can't reference C# identifiers. But you can keep the path consistent by mirroring the constant's value:

//ImportEndpoint("_lib/case-helpers")   // Endpoints._lib_CaseHelpers

var summary = await RunEndpointAsync<string>(
    Endpoints.Cases_Summarize,
    body: $"{{\"caseId\":\"{caseId}\"}}");

Renaming the source endpoint in the UI updates the constant immediately. If you forget to update the matching //ImportEndpoint(...) line, the import will fail and the compiler will tell you exactly which line.

Sharing code with AI tools

AI tools compile through the same ReplaceImports pass as endpoints, so the directive works identically inside a tool's code. A tool can import an endpoint, and an endpoint can import a tool's "library" endpoint — there is no separate "Code Module" or "Shared Library" type.

AI Tool code (function findSimilarCases):

//ImportEndpoint("_lib/case-helpers")

var args = arguments.FromJson<SearchArgs>();
var hits = Q().StartAt(N.SupportCase.Type)
              .StartAtSimilarText(args.query, indexUID: _caseTextIndex, count: args.topK)
              .AsEnumerable()
              .Select(CaseHelpers.ToDto)
              .ToList();

return new ToolCallResult(hits.ToJson());

public record SearchArgs(string query, int topK = 5);

The tool returns CaseDto objects with the same shape as the HTTP endpoints — front-end and LLM consumers see one source of truth.

"Tools and endpoints have separate import sets"

Tool code is compiled with its own implicit using statements (ChatAIToolImports), distinct from those of endpoints. Sharing types still works because the imported source is appended into both compilations, but advanced features that depend on the implicit-usings list may behave differently — pin any namespace you need with an explicit using line in your shared helper.

Rules and pitfalls

Rule Why
Imports are appended, not inlined. Declare types and static helpers in the imported file — they become available to the importer at file scope. Don't put bare statements at the top of an import-only endpoint (they would execute after the importer's body).
Cyclic imports throw. Refactor shared state into a third "leaf" endpoint and have both sides import it.
Each endpoint is imported once. Diamond imports (A → B, A → C, B → D, C → D) are safe — D is included once.
Restrict shared-library endpoints. Anything callable over HTTP is a potential attack surface. Set library endpoints to Restricted and add a Forbid guard at the top, or use a path prefix like _lib/ to make their role obvious.
Source line numbers are preserved. The //ImportEndpoint(...) line itself is kept as a comment, and imported code is appended at the end — your error messages still point at the right line in your code.
Imports are resolved at compile time. Editing an imported endpoint causes consumers to recompile on next invocation. There is no runtime "load" of code.

Referenced by

© 2026 Curiosity. All rights reserved.