Calling Agents from External MoonBit Applications
Overview
Golem can generate typed MoonBit client libraries (bridge SDKs) for calling agents from any external MoonBit application — a CLI tool, a background job, or any native MoonBit program. The generated client communicates with the Golem server’s REST API and provides a fully typed interface matching the agent’s methods.
The bridge target language is independent of the agent’s source language, so this works for agents written in any language, including MoonBit.
Step 1: Enable Bridge Generation
Add a bridge section to golem.yaml:
bridge:
moonbit:
external:
agents: "*" # Generate for all agents
# Or list specific agents:
# agents:
# - MyAgent
# - my-app:billing
outputDir: ./bridge-sdk/moonbit # Optional custom output directoryThe agents field accepts "*" (all agents), or a list of agent type names or component names (namespace:name).
Step 2: Generate the Bridge SDK
The recommended approach is to declare the bridge in golem.yaml (as shown above) and let golem build produce the SDK as part of the normal build:
golem build --yesThis produces a MoonBit module per agent type (e.g., my-agent-client/) under
bridge-sdk/moonbit/ (or golem-temp/bridge-sdk/moonbit/ when outputDir is omitted). Re-running
golem build after agent changes keeps the generated client in sync automatically.
Avoid invoking golem generate-bridge manually — it exists as a low-level escape hatch, but the manifest-driven flow above is the supported way to keep bridges configured, reproducible, and up to date.
Step 3: Add the Generated Module to a Workspace
The generated my-agent-client/ directory is a self-contained module. The generator deliberately
emits its metadata as moon.mod.json; do not rename or edit that generated file. The application
itself should use the current moon.mod format.
Create the external application in external-client/. Because local path dependencies in
moon.mod are deprecated, add external-client/moon.work with both modules:
members = [
".",
"../bridge-sdk/moonbit/my-agent-client",
]Then import the generated module and moonbitlang/async in the application’s moon.mod, using the
exact generated name/version and the async version declared by the bridge’s moon.mod.json:
name = "my-org/external-client"
preferred_target = "native"
import {
"my-agent-client@0.0.1",
"moonbitlang/async@0.21.2",
}The workspace resolves my-agent-client locally. Run moon check --target native from
external-client; current MoonBit commands fetch registry dependencies automatically, so do not
use the old project-dependency meaning of moon install.
Step 4: Use the Generated Client
Import the generated client package, the bundled runtime package, and moonbitlang/async in your moon.pkg. The moonbitlang/async import is required by MoonBit to use an async fn main entry point, even if you never reference @async directly:
import {
"moonbitlang/async" @async,
"my-agent-client/client" @client,
"my-agent-client/runtime" @runtime,
}Then call the agent. All generated constructors and methods are async and run on the moonbitlang/async event loop:
async fn main {
// Configure the Golem server connection
@client.MyAgent::configure(@runtime.Local, "my-app", "local")
// Get or create an agent instance
let agent = @client.MyAgent::get("my-instance")
// Call methods — fully typed parameters and return values
let result = agent.do_something("input")
println(result)
}Build and run the native binary:
moon build --target native
moon run --target native src/mainServer Configuration
@runtime.GolemServer has three variants:
// Local development server (http://localhost:9881)
@runtime.Local
// Golem Cloud
@runtime.Cloud("your-api-token")
// Custom deployment
@runtime.Custom("https://my-golem.example.com", "your-token")configure(server, appName, envName) sets the connection shared by every generated client in the module.
Phantom Agents
To create multiple agent instances that share the same constructor parameters, use phantom agents. The phantom id is the last argument, after the constructor parameters:
let agent = @client.MyAgent::get_phantom("my-instance", "shared-phantom-id")Or generate a random phantom id automatically:
let agent = @client.MyAgent::new_phantom("my-instance")Triggering and Scheduling
In addition to the awaiting call, each method has fire-and-forget and scheduled variants:
// Fire-and-forget: enqueue the invocation without waiting for a result
agent.trigger_do_something("input")
// Schedule the invocation for a future time: the RFC 3339 timestamp comes
// first, followed by the method parameters
agent.schedule_do_something("2026-01-01T00:00:00Z", "input")Overriding Configuration
If the agent declares locally overridable configuration, the generated client
also exposes *_with_config constructor variants. Each declared config value
becomes an optional parameter (named config_<path>) appended after the
constructor parameters; pass Some(..) to override a value or None to leave it
at its default:
// Durable: override config when getting/creating the agent
let agent = @client.MyAgent::get_with_config("my-instance", Some("override-value"))
// With an explicit phantom id (the phantom id comes before the config overrides)
let p = @client.MyAgent::get_phantom_with_config("my-instance", "phantom-id", Some("v"))
// With a fresh random phantom id
let np = @client.MyAgent::new_phantom_with_config("my-instance", None)These variants are only generated when the agent declares overridable (non-secret) configuration.
Multimodal Methods
When a method takes or returns multimodal input (a sequence of mixed-modality
items, such as text and images), the parameter or return type is an
Array[Multimodal<N>], where Multimodal<N> is a generated enum with one case
per modality:
let reply = agent.analyze([
Multimodal0::Text("describe this"),
Multimodal0::Image(@runtime.BinaryUrl("https://example.com/cat.png")),
])Unstructured Text and Binary
Schema values explicitly role-marked as unstructured text or unstructured binary map to the ergonomic
@runtime.UnstructuredText and @runtime.UnstructuredBinary wrappers, each of
which is either inline content or a URL reference:
// Inline text with an optional BCP-47 language code, or a URL
@runtime.TextInline("hello", Some("en"))
@runtime.TextUrl("https://example.com/note.txt")
// Inline bytes with an optional MIME type, or a URL
@runtime.BinaryInline(bytes, Some("image/png"))
@runtime.BinaryUrl("https://example.com/cat.png")These wrappers do not represent arbitrary bare text or binary schema values. Bare binary maps to
@runtime.AgentBinary, and bare text is not supported by this bridge mapping. When a role-marked
value restricts the allowed language codes or MIME types, the generated
client validates returned values against the allowed set and raises a
@runtime.BridgeError on a disallowed code.
Generated Module Layout
Each agent type gets its own moon module directory containing:
moon.mod.json— module metadata, depending onmoonbitlang/asyncruntime/— the bundled runtime package (<agent-client>/runtime): HTTP transport, the schema-native value model, codecs, and configurationclient/client.mbt— the generated, fully typed client package (<agent-client>/client): the agent handle, its constructors and method wrappers, and the generated MoonBit types and codecs for all custom parameter and return types
Key Points
- Bridge generation runs during
golem build— agents must be built first so their type information is available - The generated code is fully typed — method parameters and return types map to MoonBit types, and all custom types (records, variants, enums, flags, unions, multimodal, unstructured text/binary) are generated as corresponding MoonBit types
- The client targets
nativeand usesmoonbitlang/asyncfor HTTP communication; all constructors and methods areasync - The generated module is self-contained: it bundles its runtime and only depends on
moonbitlang/asyncand the MoonBit core library - Keep application metadata in
moon.mod; leave the generated bridge’smoon.mod.jsonunchanged - Resolve the generated module through
moon.work, then runmoon check --target native