Skip to content

Config Tag Reference

Every exported field in a Config / Configs struct that carries a wick:"..." tag becomes one editable row in the configs table at boot time, scoped to that module's key. Fields without a tag are ignored.

Tag grammar

Tags are semicolon-separated. A key=value pair sets a named attribute; a bare key is a boolean flag.

go
type Config struct {
    // text input + description
    Title string `wick:"desc=Card title shown in the UI."`

    // url widget + required + description
    Endpoint string `wick:"url;required;desc=API base URL. Example: https://api.example.com"`

    // dropdown with fixed options
    Mode string `wick:"desc=Conversion mode.;dropdown=uppercase|lowercase|titlecase"`

    // multi-line textarea
    Template string `wick:"desc=Prompt template.;textarea"`

    // number input (also auto-applied for int/float fields)
    MaxRows int `wick:"desc=Max rows returned per query.;required"`

    // secret/password field
    APIKey string `wick:"desc=External API key.;secret;required"`

    // native checkbox (auto-applied for bool fields)
    EnableCache bool `wick:"desc=Cache results across requests."`

    // toggle switch — clearer on/off visual; opt-in via `bool` flag
    DebugMode bool `wick:"bool;desc=Verbose logging."`

    // editable table — see kvlist section below
    Groups string `wick:"kvlist=id|name;desc=Visible group definitions."`

    // override the auto snake_case column name
    LegacyKey string `wick:"key=legacy_api_key;secret;desc=Deprecated. Kept for v1 clients."`
}

Widget reference

TagWidget renderedNotes
(none / default string)Text input
textareaTextareaMulti-line
dropdown=a|b|cSelectPipe-separated options
checkboxNative checkboxAuto-applied for Go bool fields. Compact, classic style.
bool / booleanToggle switchBoolean with clear on/off track + knob. Use when state should read at a glance.
numberNumber inputAuto-applied for int / float fields
secretPassword inputMasked; value never sent to browser. Shows •••••••• when set
emailEmail inputHTML type="email"
urlURL inputHTML type="url"
colorColor pickerHTML type="color"
dateDate pickerHTML type="date"
datetimeDate-time pickerHTML type="datetime-local"
kvlist=col1|col2Editable tableValue stored as JSON array — see below
picker=<source>Searchable typeahead with chipsValue stored as JSON [{id,name},...]. Requires the parent module to implement a LookupProvider. See below.

Modifiers (any widget)

ModifierEffect
desc=...Help text shown below the field in the admin UI
default=...Seed value used when the Go field is its zero value ("", 0, false)
requiredc.Missing() / job.Missing() returns this key until it is set
lockedRead-only in admin UI — set once at boot, not editable post-deploy
regenShows a regenerate button in admin UI — key must have a registered generator
key=custom_nameOverride the auto-derived snake_case key (InitTextinit_text)
visible_when=field:valueShow this field in the admin UI only while another field equals the named value. Use field:a|b|c (pipe-separated) to allow a set. Pure presentation hint — value is still seeded / saved normally.
mode=fixed / mode=expressionLock the workflow editor's Fixed ⇄ Expression toggle for this field. mode=fixed forces fixed-literal mode; mode=expression forces template mode. Omit the tag (default) to leave the toggle enabled. Pure presentation hint for the workflow canvas inspector — has no effect on the admin Settings page. Not persisted.
hiddenSkip the field in the default admin Settings page. Row is still seeded to DB and readable via c.Cfg(...), so runtime works normally — use for fields managed by a dedicated page (e.g. channel setup composers).

Key derivation

Field names are automatically snake-cased:

FieldKey
InitTextinit_text
APIBaseURLapi_base_url
MaxRetriesmax_retries

Override with key=... when the derived name is wrong or you need to keep a legacy key stable.

kvlist — editable table widget

Use kvlist when a config field holds a dynamic list of structured rows — a set of IDs with labels, a mapping of endpoints, a table of question groups. Not for free-form text; use textarea for that.

go
type Config struct {
    // multi-column: [{"id":"1","name":"Sales"},{"id":"2","name":"Support"}]
    Groups string `wick:"kvlist=id|name;desc=Visible question groups."`

    // single-column (bare kvlist): [{"value":"ID_001"},{"value":"ID_002"}]
    Allowlist string `wick:"kvlist;desc=Allowed sender IDs."`
}

Value format — stored in the configs.value column as a JSON array of string-keyed objects:

json
[{"id":"1","name":"Sales"},{"id":"2","name":"Support"}]

Read in Go:

go
var rows []map[string]string
if err := json.Unmarshal([]byte(c.Cfg("groups")), &rows); err == nil {
    for _, row := range rows {
        fmt.Println(row["id"], row["name"])
    }
}

Admin UI behaviour: renders an inline editable table. Rows can be added with + Add Row (or Tab from the last cell) and removed with ×. Changes auto-save 800 ms after the last keystroke — no Save button needed.

When to use kvlist

  • Two or more columns per row → kvlist=col1|col2|col3
  • One column only → bare kvlist (defaults to a value column)
  • Free-form multi-line text → use textarea instead

picker — searchable typeahead widget

Use picker when the legal values come from an upstream directory (a Slack workspace, a Discord guild, a customer table) and the operator should pick chips by name instead of pasting raw IDs.

go
type Config struct {
    AllowedUsers    string `wick:"picker=slack.users;desc=Allowed users."`
    AllowedChannels string `wick:"picker=slack.channels;desc=Allowed channels."`
}

Value format — identical to a 2-column kvlist=id|name:

json
[{"id":"U123","name":"Yoga"},{"id":"U456","name":"Deva"}]

So any whitelist check is just id-membership, and the same parser reads either widget.

Lookup source — the value after = is a registry key (e.g. slack.users). The parent module — typically a channel — must implement LookupProvider:

go
type LookupProvider interface {
    Lookup(source, query string) ([]LookupItem, error)
}
type LookupItem struct{ ID, Name string }

The admin UI debounces 250 ms per keystroke and fires GET /channels/<slug>/lookup?source=<src>&q=<q>. Implementations should cap results (~20) and cache aggressively — see slack/lookup.go for a 60 s in-memory cache example.

Admin UI behaviour: a search input with a debounced dropdown; click → chip. Chips have an × to remove. Like kvlist, changes auto-save with no Save button.

When to use picker vs kvlist

  • IDs come from an upstream directory the user knows by name → picker
  • IDs are arbitrary / freeform / configured locally → kvlist=id|name

visible_when — conditional fields

Hide a field from the admin form until another field equals a target value:

go
type Config struct {
    Mode    string `wick:"dropdown=all|whitelist;default=all"`
    Allowed string `wick:"picker=slack.users;visible_when=mode:whitelist;desc=Allowed users."`
}

For a set of allowed values use a pipe-separated list (OR semantics):

go
type Config struct {
    Method string `wick:"dropdown=GET|POST|PUT|PATCH|DELETE"`
    Body   string `wick:"textarea;visible_when=method:POST|PUT|PATCH|DELETE;desc=Request body."`
}

The field still seeds and persists normally — visible_when only toggles the form row. Useful for cutting noise in config pages with many feature-flagged dependants.

Built with ❤️ by a developer, for developers.