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"`

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

    // toggle switch (bool/boolean are aliases of the same control)
    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
checkbox / bool / booleanToggle switchAll three render the same on/off toggle (track + knob). Auto-applied for Go bool fields; the tags are aliases.
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.
html=<op>Server-rendered widgetThe widget fetches markup from connector op <op> ({html:"..."}) and renders it read-only. Buttons in that HTML drive behaviour via a data-op convention — see below. The core stays domain-agnostic.

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).
group=Title / group=Title|Description / group=Title|Description|collapsedGroup fields into a titled card on the admin Settings page. All fields sharing the same Title are rendered together under one card, in first-seen order. Fields with no group fall into the default "Configuration" card. The optional Description (pipe-separated) is written once at the top of the card — use it for a one-sentence summary of the group's purpose. A 3rd |collapsed segment makes the card start collapsed (click the header to expand) — use for advanced/rarely-edited groups so the page opens clean; Title||collapsed collapses with no description. Only the group's first-seen field needs to carry the description / collapsed flag. Not persisted. Pure presentation.

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

html — server-rendered widget

Use html=<op> when a field's UI is dynamic and owned by the connector, not a fixed input — a live status list, an install/download picker, anything where the backend should decide the layout and the available actions. The core renders whatever markup the op returns and stays completely domain-agnostic (it never knows the field is a "browser" or a "model").

go
type Config struct {
    // The widget fetches markup from the "browser_status" op and renders it.
    Browser string `wick:"html=browser_status;default=chromium;desc=Browser engine."`
}

The op returns markup{"html": "<div>…</div>"}. It runs through the manager-only /test path (admin/tag-gated), never MCP. Since the op is still LLM-callable by default, mark maintenance-style ops AdminOnly in the ops table so the agent can't invoke them.

data-op convention — the widget wires two behaviours onto any element in the returned HTML:

AttributeEffect on click
data-op="__select" data-arg="<value>"Store <value> as this field's value (reserved — no op call).
data-op="<opKey>" data-arg="<value>"Run connector op <opKey> (with <value> passed as its argument) via /test, then re-fetch the HTML so state updates.
go
// Example browser_status op output — the connector owns all layout + logic:
// <div data-op="__select" data-arg="chromium">Chromium ✓ v133  [selected]</div>
// <div>Firefox — <button data-op="browser_install" data-arg="firefox">Download</button></div>

So a Download button installs then refreshes; a row click selects. No SSE — long actions are covered by the re-fetch (poll the status op yourself inside a loop if you want live progress). The markup is admin-only server content rendered in the admin Settings page, so it is {@html}-rendered directly; keep it to connector-authored markup, never user input.

Named inputs — any element with a name inside the widget's markup is collected as input.<name> when an op runs, so a connector can render its own form (e.g. a textarea, a checkbox-based policy editor) and read what the user entered. Collection follows plain HTML form semantics: text/textarea/select send .value; a checkbox or radio sends its value attribute (default "on") only when checked — unchecked contributes nothing, so a boolean setting must be read as "key present", not "value truthy".

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.

group — config field grouping

Use group to cluster related fields under a titled section card on the admin Settings page. The first field in a group supplies the optional description; subsequent fields with the same title just carry group=Title.

go
type Config struct {
    // First field in a group — sets the title + description for the card.
    Mode    string `wick:"dropdown=socket|http;group=Connection|Transport credentials. Socket mode needs bot + app token; HTTP mode needs bot token + signing secret.;desc=Connection mode."`
    // Subsequent fields — same title, no description needed.
    Token   string `wick:"secret;group=Connection;desc=Bot token (xoxb-...)."`
    Secret  string `wick:"secret;group=Connection;desc=Signing secret. Required for http mode."`

    // A separate group for access controls.
    UsersMode    string `wick:"dropdown=all|whitelist;group=Access Control|Who may trigger the agent.;desc=Restrict which users can trigger."`
    AllowedUsers string `wick:"picker=slack.users;visible_when=users_mode:whitelist;group=Access Control;desc=Allowed users."`
}

Cascading visible_when: a picker or kvlist field inside a group also respects visible_when normally — it only appears when its dependency value matches, even inside the group card.

Fields without a group fall into the default "Configuration" card, which is always rendered last.

Collapsed cards — append |collapsed as the 3rd segment on the group's first field to start that card closed. The admin renders it as an expandable section (a native <details> — a click on the header toggles it). Use for advanced or rarely-edited groups so the page opens uncluttered:

go
type Config struct {
    // Essentials — always visible.
    Browser  string `wick:"dropdown=chromium|firefox|webkit;group=Browser|The knobs most setups need.;desc=Engine."`
    Headless bool   `wick:"bool;group=Browser;desc=Headless mode."`

    // Advanced — card starts collapsed (Title|Description|collapsed).
    ProxyServer string `wick:"group=Network|Route traffic through a proxy.|collapsed;desc=Proxy URL."`
    ProxyBypass string `wick:"group=Network;desc=Domains to bypass."`

    // Collapsed with no description: Title||collapsed
    Channel string `wick:"group=Custom binary||collapsed;desc=Branded channel."`
}

Grouping vs hidden

group organises visible fields. hidden hides a field entirely regardless of groups. A field can be both — it simply won't appear in any group card.

Built with ❤️ by a developer, for developers.