Adding Typed Configuration to an Agent (MoonBit)
The MoonBit SDK has full support for code-first typed configuration via #derive.config, @config.Config[T], and @config.Secret[T], equivalent to the Rust SDK’s ConfigSchema / #[agent_config].
1. Define Config Types with #derive.config
Annotate record structs with #derive.config. This auto-generates ConfigField trait implementations (via golem_sdk_tools) for schema collection and typed loading. Config types can be nested but must not be generic.
#derive.config
pub(all) struct DatabaseConfig {
host : String
port : UInt
timeout : UInt64
}
#derive.config
pub(all) struct AppConfig {
app_name : String
debug : Bool
database : DatabaseConfig // nested config type
}Supported field types
All primitive types that implement ConfigField: String, Bool, Int, UInt, Int64, UInt64, Float, Double, Byte, Char, Bytes, plus T? (optional), Array[T], Result[T, E], and nested #derive.config structs.
2. Inject Config into the Agent Constructor
Add a @config.Config[T] parameter to the agent’s new function. The platform loads and validates all config values at agent construction time:
#derive.agent
pub(all) struct MyAgent {
config : @config.Config[AppConfig]
}
fn MyAgent::new(config : @config.Config[AppConfig]) -> MyAgent {
{ config }
}Access config values through self.config.value:
pub fn MyAgent::do_work(self : Self) -> Unit {
let cfg = self.config.value
if cfg.debug {
@log.debug("Connected to \{cfg.database.host}:\{cfg.database.port}")
}
}3. Add Secrets with @config.Secret[T]
Wrap sensitive fields in @config.Secret[T]. Secrets are stored per-environment and fetched dynamically (not snapshot-cached like regular config):
#derive.config
pub(all) struct DatabaseConfig {
host : String
port : UInt
password : @config.Secret[String] // secret field
}Access the current secret value with get!():
pub fn MyAgent::connect(self : Self) -> Unit {
let db = self.config.value.database
let password = db.password.get!()
// use password...
}4. Provide Config Values
In golem.yaml (application manifest)
Config values are typed — they match the schema defined in code:
agents:
MyAgent:
config:
app_name: "my-app"
debug: true
database:
host: "localhost"
port: 5432
timeout: 30000Secrets via secretDefaults in the manifest
secretDefaults:
local:
database:
password: "{{ DB_PASSWORD }}"Secrets via CLI
golem agent-secret create database.password --secret-type string --secret-value "pwd"
golem agent-secret update-value database.password --secret-value "new-pwd"5. How It Works Under the Hood
#derive.configis processed bygolem_sdk_tools(seeconfig_types.mbtandconfig_emit.mbt).- For each annotated struct, the tool generates
ConfigFieldtrait implementations with:collect_entries(path)— declares the config schema to the platform (field paths and WIT types)load(path)— loads typed values from the host at runtime
@config.load_config()is called during agent construction, which recursively loads all fields.Secret[T]fields store only the key path and expected type; the actual value is fetched on eachget!()call via@host.get_config_value.- The platform validates that all required config and secrets are provided at deploy time.
Key Constraints
#derive.configdoes not support generic (parameterized) structs- Config types cannot have cycles (validated at build time)
#derive.configtypes cannot be nested insideOption,Array,Result, orTuplecontainers — only direct nesting of one config struct inside another is allowedSecretcannot wrap a#derive.configtype — only primitive/leaf types- Config values (non-secret) are loaded once at construction; secrets are fetched dynamically on each
get!()call