Reference

Profiles

Reusable, admin-editable config bundles — one dropdown on a FlowDSL node replaces eight hand-configured fields. Works for LLM chat, guard, databases, FTP, storage, any connection-bearing node.

Profiles

A profile is a named, reusable config bundle that FlowDSL nodes reference by id instead of hand-configuring each field. They live in MongoDB (one row per profile), are edited from /admin/profiles, and fan-out cross-worker via the EventBus so operators never need to restart containers to pick up a change.

The goal: on a redelay/llm-chat node you no longer configure providerID + model + temperature + maxTokens + systemPrompt + replyStyle + examplesPolicy per step. Instead, you pick assistant-fast from the profile dropdown and leave the other fields empty. The handler merges the profile's config underneath whatever you did set on the node — per-node fields still override.

Same pattern applies to every other connection-bearing node you'll add — DB, FTP, storage, SMTP. One profile definition, one dropdown, zero copy-paste.

Architecture

text
module.yaml: profiles: [...] ─────────► ProfileKind (IR)
                                              │
                                              ▼
ProfilesProvider interface ─► settings.Module registers live profiles
                                              │
                                              ▼
                                   settings.ProfilesService
                                   (MongoDB + cache + bus)
                                              │
           ┌──────────────────────────────────┼──────────────────────────────────┐
           ▼                                  ▼                                  ▼
   /admin/profiles API              IRBase.FlowDSLNodes()              modules.ResolveProfile
   (CRUD + duplicate)               (injects profile enum)           (handler runtime merge)

Declaring a profile kind

A module owns zero or more profile kinds. Each kind specifies the SettingField shape for one profile of that kind — the admin UI renders an editor from it, reusing the same form components as module settings.

Add a profiles: section to module.yaml:

yaml
id: ai-llm
name: AI LLM
profiles:
  - id: llm-chat                       # consumed by x-profile-kind on node settings
    name: LLM Chat Profile
    description: Named preset bundling provider + model + tuning + prompt.
    icon: lucide:message-circle
    fields:
      - key: providerID
        label: Provider
        type: string
        required: true
        options_ref: llm.providers     # live dropdown from go-ai registry
      - key: apiKey                    # credential — masked in responses
        label: API key
        type: string
        sensitive: true
      - key: baseURL                   # multi-account support: each profile has its own endpoint
        label: Base URL
        type: string
      - key: model
        label: Model
        type: string
      - key: temperature
        label: Temperature
        type: number
      - key: systemPrompt
        label: System prompt
        type: textarea

That's it — no Go code required. IRBase auto-exposes the kind via the ProfilesProvider interface, and settings.Module picks it up during Configure.

options_ref — live enum sources

Set options_ref: <source-name> on a field to have the server populate its options from a live registry at serve time. Sources are registered by the owning module via modules.RegisterOptionsSource(name, func). Ships with:

SourceValuesRegistered by
llm.providersregistered LLM factory ids (openai, anthropic, gemini, ovh, ollama)go-ai/llm/module
guard.guardsregistered guard factory ids (qwen3guard, noop)go-ai/guard/module

Inline options: [...] always wins over options_ref — useful for "live list with one curated extra" scenarios.

setting_ref — reuse existing settings

Defining a profile field that mirrors an existing setting is verbose. Use setting_ref: <module>/<key> to inherit label, description, type, sensitive flag, env_var, default, and options from the referenced SettingField:

yaml
profiles:
  - id: llm-chat
    fields:
      - setting_ref: ai-llm/ovh_api_key  # inherits label/type/sensitive/env_var
      - key: model                       # inline definition as usual
        label: Model
        type: string

Inline keys on the same field override specific attributes from the reference. Handy for credential fields — declare env_var + sensitive once in settings:, reference it from every profile kind that needs credentials for the same backend. Separator accepts / or . for YAML style preference.

sensitive: true on fields

Works identically to module settings — see the Settings section. List/get responses return ••••••••; the UI shows a password input with a Change button; unchanged fields send the token back and the server preserves the stored value.

Multiple accounts per provider

A single OVH/OpenAI/Anthropic account rarely covers every environment. Because each profile row owns its own apiKey + baseURL + model, one kind supports unlimited accounts side-by-side:

text
ovh-prod-eu       → OVH EU endpoint,  token A,  Meta-Llama-3.3
ovh-prod-us       → OVH US endpoint,  token B,  Meta-Llama-3.3
ovh-sandbox       → OVH sandbox,      token C,  Mistral-7B
ollama-dev        → Ollama localhost, (no key), qwen2.5:3b

Each is a single row under the same kind; the redelay/llm-chat profile dropdown lists them all.

Consuming profiles on a FlowDSL node

Picker property name. Studio locates the profile picker via the x-profile-kind marker, not a fixed key name. Use whatever reads naturally for your node — profile, connection, preset, credentials, … All examples in this page use profile.

Any node that wants a profile selector adds one setting:

yaml
settings_schema:
  type: object
  # Hint renderers to put profile above the overridable fields.
  x-ui:
    order: [profile, providerID, model, temperature, systemPrompt]
  properties:
    profile:
      type: string
      title: Profile preset
      x-order: 0
      x-profile-kind: llm-chat       # which kind's profiles to list
      # List which sibling fields this profile supplies defaults for —
      # the admin UI can visually dim them when a profile is selected.
      x-overrides: [providerID, model, temperature, systemPrompt]
      description: >
        Named preset. When set, the fields below default to the
        profile's value; any field you explicitly fill overrides.
      enum: []                       # populated at spec-build time
    providerID:
      type: string
      x-order: 1
      x-profile-overridable: true    # dim when a profile is selected
      description: "…defers to the selected profile when blank."
      enum: []

IRBase.FlowDSLNodes looks for x-profile-kind on every profile setting and injects the current live profile ids via modules.ListProfileIDs. Studio renders a normal enum dropdown with no per-node code.

Renderer hints (forward-compatible):

HintLocationPurpose
x-ui.ordertop-level schemaExplicit render order for fields — wins over alphabetical sort when the renderer supports it
x-orderper-propertySame purpose, per-field form. Renderers that respect one usually respect the other
x-overrideson profileArray of sibling keys the profile supplies defaults for — lets the UI render a "profile is overriding N fields" badge
x-profile-overridableon any fieldSignal that this field's blank value defers to the selected profile. UI can dim or watermark

At runtime, the handler calls:

go
profileID, _ := step.Node.Config["profile"].(string)
if profileID != "" {
    if profile := modules.ResolveProfile(ctx, "llm-chat", profileID); profile != nil {
        step.Node.Config = modules.MergeProfileIntoConfig(step.Node.Config, profile)
    }
}

MergeProfileIntoConfig layers the profile values underneath — any key already present in step.Node.Config wins. Per-node overrides always beat profile defaults.

Consuming profiles outside FlowDSL

Profiles are not just for nodes. Any module built on go-ai can resolve its own llm-chat profile via modules.ResolveProfile, so different features run different models — configured independently. Translations can run a best-quality model while a workout-analysis feature runs a cheaper/faster one, each pointing at its own profile.

go-modules/ai (the translate/generate module) does exactly this: it exposes an ai/profile setting (env AI_PROFILE) that names the llm-chat profile to use. When set, that profile overrides the module's flat provider / model / temperature settings; when blank, the flat settings apply. A second go-ai consumer sets its own <module>/profile, so the two features never share a model unless you point them at the same profile.

Admin API

All endpoints live under /api/v1/admin/profiles/... and require the admin:access permission plus one of the profiles:* permissions declared by the settings-admin module.

MethodPathPurpose
GET/admin/profiles/List every profile (sensitive fields masked)
GET/admin/profiles/kindsKinds discovered from the registry (with field schemas)
GET/admin/profiles/kinds/{kind}Profiles of a single kind
POST/admin/profiles/Create / upsert a profile
GET/admin/profiles/{module}/{kind}/{id}Get one profile
PUT/admin/profiles/{module}/{kind}/{id}Update a profile
DELETE/admin/profiles/{module}/{kind}/{id}Delete a profile
POST/admin/profiles/{module}/{kind}/{id}/duplicateDuplicate — body is optional DuplicateOverrides

Sensitive fields

Any field declaring sensitive: true in its schema is masked with the literal token •••••••• in list/get responses. The admin UI shows a password-style input with a Change button — clicking it clears the token and accepts a new value. Unchanged fields send the masked token back to the server, which detects it and preserves the stored value in MongoDB.

Same flag already applies to regular settings (ir.SettingField.Sensitive). The masking layer is shared.

Duplicate

shell
curl -X POST /admin/profiles/ai-llm/llm-chat/assistant-fast/duplicate \
  -H "Content-Type: application/json" \
  -d '{
    "newProfileId": "assistant-backup",
    "newName": "LLM backup",
    "configPatches": { "providerID": "ollama", "model": "qwen2.5:3b" }
  }'

The server clones the source profile, applies the patches on top (sensitive values are copied as-is), picks an auto-incrementing <src>-copy[-N] id when newProfileId is empty, and returns the persisted clone. Admin UI ships a duplicate dialog that pre-fills the clone's name + all field values for quick tweaks.

Common patterns:

  • LLM backup — duplicate assistant-fast, switch providerID → ollama, name → "LLM backup".
  • Tuning variant — duplicate, bump temperature from 0.3 → 0.7, name → "reasoning — deep".
  • Sandbox — duplicate production, swap credentials for a staging key.

Usage aggregation by profile

Every LLM call stamps profileId onto the usage ledger row (LLMUsage.ProfileID). The admin /ai/breakdowns page now offers Profile as a breakdown dimension alongside Provider, Model, Flow, Node, User, Assistant — sort profiles by cost, token count, latency, or error rate to see which preset is actually driving spend.

Under the hood, UsageContext.ProfileID is populated by the llm-chat handler right after it merges the profile config; the recorder middleware writes it to MongoDB. Query filter:

text
GET /admin/llm/usage/by?field=profileId&from=2026-04-01T00:00:00Z
GET /admin/llm/usage?profileId=ovh-prod-eu   # filter usage rows to one profile

Cross-worker cache invalidation

Writes publish profiles.updated on the EventBus. Every worker subscribes with a host-unique GroupID (profiles-invalidator-{hostname}) so it's fan-out, not load-balanced — each peer invalidates its local cache and re-fetches on the next request. No container restart, no stale reads.

Admin UI

/admin/profiles — unified page grouped by kind. Each kind card shows a count, a "New" button, and links to every existing profile of that kind. Each card also links to the owning module's /settings/<module> page for cross-context tweaks.

/admin/profiles/{module}/{kind}/{id} — profile editor. Identity section (profileId, name, description) + configuration section rendered from the kind's fields[]. Sensitive fields use the mask + Change flow.

Duplicate dialog — reachable from the editor's action bar. Pre-populates a new id + name and shows the full field editor so operators can tweak in-flight before saving.

Cross-links on settings pages — when a module declares any profile kinds, a banner appears at the top of /settings/<module>/... linking to the relevant /profiles kinds. Operators manage credentials in either place; the links keep both contexts one click apart.

Worked example — replacing per-node config

Before (hand-configured every time):

yaml
- id: chat
  action_ref: redelay/llm-chat
  config:
    providerID: ovh
    model: Meta-Llama-3_3-70B-Instruct
    temperature: 0.3
    maxTokens: 1024
    replyStyle: short
    examplesPolicy: when_asked
    systemPrompt: "You are the Project Assistant for a Redelay-based application..."

After (referencing a profile):

yaml
- id: chat
  action_ref: redelay/llm-chat
  config:
    profile: assistant-fast
    systemPrompt: "You are the support assistant for acme.com."  # per-flow override

The handler resolves assistant-fast (provider, model, temperature, maxTokens, replyStyle, examplesPolicy) and merges the profile values underneath. systemPrompt set on the node wins. One dropdown + one override instead of seven explicit fields.

Future kinds

Any node that needs credentials or connection config is a candidate:

KindOwnerTypical fields
llm-chatai-llmproviderID, model, temperature, maxTokens, systemPrompt
llm-guardai-guardguardID, role, onError
db-sqlgo-module-sql (planned)driver, host, port, database, user, password (sensitive)
ftp-servergo-module-ftphost, port, user, password (sensitive), passive
smtpgo-module-emailhost, port, user, password (sensitive), from
s3-bucketgo-modules/storageendpoint, region, bucket, accessKey (sensitive), secretKey (sensitive)
search-configgo-modules/searchbackend, prefix, embeddingProvider, embeddingModel

Each additional kind is 15 lines of YAML plus a tiny handler change (three lines to call ResolveProfile + MergeProfileIntoConfig). The admin UI, CRUD, invalidation, sensitive masking, and duplicate flow come for free.