Welcome to the new Golem Cloud Docs! 👋
Adding Typed Configuration to an Agent (MoonBit)

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: 30000

Secrets via secretDefaults in the manifest

secretDefaults:
  local:
    - path: [database, password]
      value: "{{ 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

  1. #derive.config is processed by golem_sdk_tools (see config_types.mbt and config_emit.mbt).
  2. For each annotated struct, the tool generates ConfigField trait 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
  3. @config.load_config() is called during agent construction, which recursively loads all fields.
  4. Secret[T] fields store only the key path and expected type; the actual value is fetched on each get!() call via @host.get_config_value.
  5. The platform validates that all required config and secrets are provided at deploy time.

Key Constraints

  • #derive.config does not support generic (parameterized) structs
  • Config types cannot have cycles (validated at build time)
  • #derive.config types cannot be nested inside Option, Array, Result, or Tuple containers — only direct nesting of one config struct inside another is allowed
  • Secret cannot wrap a #derive.config type — only primitive/leaf types
  • Config values (non-secret) are loaded once at construction; secrets are fetched dynamically on each get!() call