Skip to content

MCP API

Klaxon exposes an MCP server over HTTP on 127.0.0.1 (ephemeral port). Protocol version: 2025-03-26.

Endpoints

MethodPathAuthPurpose
POST/mcpBearerJSON-RPC 2.0 requests (single or batch)
GET/mcpBearerSSE notification stream
DELETE/mcpBearerTerminate session
GET/mcp/discoverNoneReturns { url, bearer, protocol_version }
GET/healthNoneReturns ok

Session Lifecycle

  1. Send initialize — response includes an Mcp-Session-Id header.
  2. Include Mcp-Session-Id in all subsequent requests.
  3. DELETE /mcp with the session ID to terminate.

Per-agent identity

Every authenticated request can carry an x-client-id header to identify the calling agent (e.g. claude-code, claude-desktop, bot-deploybot). The backend persists every observed client into a mcp_agents table:

  • First-seen / last-seen timestamps
  • Total call count + per-day call counter
  • Most recent tool name

Items created by an agent are tagged with the calling client_id in their agent_id column. The klaxon/agents resource and the klaxon.list tool both expose this for agent-specific filtering. Calls without an x-client-id header are bucketed under the literal string unknown.

Rate limiting

Tool calls are subject to a per-agent sliding window: 60 calls per minute per x-client-id by default. Read methods (initialize, tools/list, resources/list, resources/read) are unmetered.

When a client exceeds the cap, the JSON-RPC response is:

json
{
  "jsonrpc": "2.0",
  "id": 42,
  "error": {
    "code": -32003,
    "message": "rate limited; retry in 8421ms",
    "data": {
      "reason": "rate_limited",
      "retry_after_ms": 8421
    }
  }
}

Agents should back off for retry_after_ms and try again. The window is purely in-memory — restart the app to reset counters.

Lenient parameter coercion

Some MCP clients (notably Claude Code's claude CLI) JSON-stringify complex parameters before sending them. So tags: ["a","b"] arrives on the wire as the literal string "[\"a\",\"b\"]", priority: 2 as "2", and form: {...} as a string-encoded object. Strict as_array / as_i64 / as_object checks would silently drop or reject these.

Klaxon's parsers accept both the native JSON form AND the string-encoded form for every complex parameter:

ParameterNativeStringified
tags["a","b"]"[\"a\",\"b\"]"
actions[{...}]"[{...}]"
choices["a","b"]"[\"a\",\"b\"]"
related_to["uuid"]"[\"uuid\"]"
form{...}"{...}"
metadata{...}"{...}"
priority2"2"
ttl_ms30000"30000"
waittrue"true"
include_*true"true"
limit/offset100"100"

If you control the MCP client, prefer the native JSON form — the error messages on type failures are friendlier. The string-encoded fallback is purely a compatibility shim for clients you don't control.

Wait semantics (wait: true)

klaxon.notify, klaxon.ask, klaxon.confirm, and klaxon.choose all support an optional wait: true parameter. When set, the JSON-RPC tool call blocks until the user responds, and the response is returned in the tool result. This lets agents pause execution until the user has reviewed something — no polling required.

Toolwait: true returnswait: false (default) returns
klaxon.notify{ id, response: null } after ack/dismiss{ id } immediately
klaxon.ask{ id, response: <form values> } after submit{ id } immediately
klaxon.confirm{ id, response: { confirmed: true|false } }{ id } immediately
klaxon.choose{ id, response: { choice: "<value>" } }{ id } immediately

The payload is delivered inside the standard MCP content[0].text field as a JSON-encoded string. Agent code should JSON.parse(result.content[0].text) to get the { id, response } object. (For backwards compatibility the same payload is also exposed as sibling fields on result, but only the content array is forwarded to the LLM by spec-compliant MCP clients.)

The default wait timeout is 5 minutes. If you pass ttl_ms, it serves as both the wait deadline AND the auto-dismiss TTL — they're the same number. So ttl_ms: 30000 with wait: true means "block for at most 30 seconds, and if the user hasn't responded by then, auto-dismiss the item AND return a timeout error to the agent". Without wait, ttl_ms is just the auto-dismiss TTL and the call returns immediately.

On timeout, dismissal, or expiry, the tool call returns a JSON-RPC error with a structured data field so the agent can branch without parsing the message:

json
{
  "error": {
    "code": -32603,
    "message": "wait timeout after 300000ms",
    "data": {
      "id": "<uuid>",
      "reason": "timeout",
      "timeout_ms": 300000
    }
  }
}

reason is one of: "timeout", "dismissed", "expired", "channel_closed".

Channels and tags

All four creation tools (klaxon.notify, klaxon.ask, klaxon.confirm, klaxon.choose) accept two optional categorization fields:

  • channel: string — a single label like "deploy" or "code-review". The Main window's sidebar dynamically lists every unique channel as a clickable filter. Items without a channel still appear under the standard filter views.
  • tags: string[] — free-form labels rendered as small chips on item cards and the detail header. Useful for cross-cutting concerns like ["urgent", "frontend"].

Both are persisted to SQLite and survive restarts. There is no channel-creation step — just set the string and the sidebar populates itself. Per-channel notification rules (mute / errors-only / sound override) are configured by the user in Settings → Notifications and live in the channel_rules table — agents don't need to do anything special to respect them.

Item relationships

Every creation tool accepts two optional relationship fields:

  • parent_id: string — UUID of an existing klaxon. Adds this item to that item's thread, surfacing both via the klaxon/thread/{id} resource. The parent must exist or the call returns an error.
  • related_to: string[] — symmetric "see also" links. Cheaper than threading because there's no hierarchy. UUIDs aren't validated against existing items so cross-references can dangle.
json
{
  "level": "info",
  "title": "Build retried, succeeded",
  "message": "Run #142 fixed by #143",
  "parent_id": "abc123-...",
  "related_to": ["def456-...", "ghi789-..."]
}

Tools

klaxon.notify

Create a non-interactive notification.

json
{
  "level": "info | warning | error | success",
  "title": "string",
  "message": "string",
  "channel": "deploy",
  "tags": ["staging", "frontend"],
  "ttl_ms": 60000,
  "wait": false,
  "parent_id": "<uuid>",
  "related_to": ["<uuid>", "<uuid>"],
  "actions": [
    { "kind": "ack",      "id": "ok",   "label": "Got it" },
    { "kind": "open_url", "id": "docs", "label": "View",  "url": "https://..." },
    { "kind": "run_tool", "id": "run",  "label": "Execute", "tool": "name", "arguments": {} }
  ]
}

Returns { id: "<uuid>" }. With wait: true, blocks until the user acks or dismisses, then returns { id, response: null }.

klaxon.ask

Create an interactive notification with a form schema. The form stays open until the user submits a response, available via the klaxon/answer/{id} resource.

TIP

For simple yes/no or "pick from a list" prompts, use klaxon.confirm and klaxon.choose — they're 1-line wrappers that don't require building a full form schema.

The full form schema is published in the tool's inputSchema so MCP clients can introspect every supported field type. Required keys at each level:

  • form: id, title. Then either fields (a flat list) OR pages (a multi-page wizard) — both default to empty if omitted, but you need at least one to render anything.
  • field: id, label, type (the discriminator)
  • page: id, fields
  • option: value (or just a plain string shorthand)
json
{
  "level": "info",
  "title": "Approve Change",
  "message": "Review the proposed diff",
  "channel": "code-review",
  "tags": ["pr-142"],
  "wait": true,
  "form": {
    "id": "review",
    "title": "Code Review",
    "description": "Approve or reject",
    "fields": [
      { "type": "diffapproval", "id": "decision", "label": "Diff", "diff": "+new\n-old" }
    ]
  },
  "ttl_ms": null
}

klaxon.ask also accepts a template: "<name>" field as a mutually-exclusive alternative to form. The named template is hydrated from the klaxon_templates table; passing both form and template is an error. See klaxon.template_save below.

klaxon.confirm

Convenience wrapper for yes/no questions. Builds a single-radio form internally.

json
{
  "level": "warning",
  "title": "Delete file?",
  "message": "src/old.ts will be removed.",
  "yes_label": "Delete",
  "no_label": "Keep",
  "channel": "cleanup",
  "tags": ["destructive"]
}

Returns { id: "<uuid>" }. With wait: true, the response is unwrapped to a real { confirmed: true | false } boolean.

klaxon.choose

Convenience wrapper for "pick one from a list". Accepts either a string array or full {value, label} objects.

json
{
  "level": "info",
  "title": "Which environment?",
  "message": "Pick a deployment target",
  "choices": ["dev", "staging", "prod"],
  "channel": "deploy"
}

With custom labels:

json
{
  "choices": [
    { "value": "dev",  "label": "Development" },
    { "value": "prod", "label": "Production" }
  ]
}

Returns { id: "<uuid>" }. With wait: true, the response is { choice: "<value>" }.

klaxon.update

Patch fields on an existing klaxon item without creating a new one. Designed for long-running operations: an agent can create an info notification at start, call klaxon.update periodically to refresh the message (e.g. progress text) and the level (info → success/error on completion), and never need to spawn a second item.

json
{
  "id": "<uuid>",
  "level": "success",
  "title": "Build complete",
  "message": "✓ 142 tests passed in 38s",
  "channel": "ci",
  "tags": ["build-1234"],
  "actions": [],
  "priority": 3,
  "deadline_at": "2026-04-08T17:00:00Z",
  "metadata": { "deploy_sha": "abc1234", "jira_ticket": "OPS-99" }
}

Only the fields you pass are changed; omitted fields are left as-is. Two nullable fields support an explicit clear:

  • channel: null clears the channel; omit to leave untouched
  • deadline_at: null clears the deadline; omit to leave untouched

tags, actions, and metadata are full replacements (not merges) when present. Returns { id }.

The updated_at column is auto-bumped by a SQL trigger on every mutation, so the main window's "Recently active" sort order picks up updates immediately.

klaxon.list

Query klaxon items with optional filters. The marquee read tool — fixes the ~60% of agent-side patterns that previously required fetching the open list and filtering client-side.

json
{
  "channel": "deploy",
  "tags": ["staging", "frontend"],
  "level": "error",
  "status": "open",
  "agent_id": "claude-code",
  "created_after":  "2026-04-08T00:00:00Z",
  "created_before": "2026-04-08T23:59:59Z",
  "include_archived": false,
  "include_withdrawn": false,
  "include_snoozed": false,
  "limit": 100,
  "offset": 0
}

All filter fields are optional. Most are AND-combined; tags is OR-matched — an item matches if ANY of its tags appears in the requested set.

limit defaults to 100, max 500. offset defaults to 0. The three soft-hide flags — include_archived, include_withdrawn, and include_snoozed — all default to false. Snoozed items in particular are hidden by default because the user-facing main window does the same thing; agent queries should match the user's mental model unless the agent is specifically auditing snoozed work.

The result is delivered inside content[0].text as a JSON-encoded { items, count, limit, offset } object — agents should JSON.parse(result.content[0].text) to read it.

klaxon.dismiss_many / klaxon.archive_many / klaxon.mark_viewed_many

Bulk apply an action to every item matching the filter. Same filter shape as klaxon.list. All three tools refuse to run unless at least one filter constraint is set — no "dismiss everything" without explicit intent.

json
{
  "channel": "deploy",
  "level": "info"
}

Returns { affected, ids } inside content[0].text. Each affected row is audited individually so the trail attributes the bulk action to each touched item.

A single notifications/klaxon SSE event with type: "bulk_updated" is emitted for the whole batch (one event per call, not N).

klaxon.withdraw

Soft-retract an item posted by an agent. Distinct from klaxon.dismiss (which the user might interpret as "I dealt with it") — withdraw is the agent's "never mind, the underlying problem self-healed" path. Useful in auto-retry loops where the agent posts a blocker, then the work succeeds before anyone sees it.

json
{ "id": "<uuid>", "reason": "auto-retry succeeded" }

Sets the item's status to withdrawn (a lifecycle state mutually exclusive with open/answered/dismissed/expired). Hidden from default klaxon.list queries unless include_withdrawn: true. The optional reason is recorded in the audit log.

Returns { ok: true }.

klaxon.snooze / klaxon.unsnooze

Hide an item from the user's default views until a future timestamp. Does NOT change status — snooze is purely a soft-hide flag, orthogonal to status, archived_at, and withdrawn. The item stays in whatever lifecycle state it was in (open, answered, etc.); only the snoozed_until column is touched.

json
// klaxon.snooze
{ "id": "<uuid>", "until": "2026-04-08T22:00:00Z" }

until must be RFC3339. Relative durations like "1h" are NOT supported — agents should compute the absolute time client-side.

A 60-second background sweep on the server clears snoozed_until when the deadline passes (still no status change). The user-facing main window hides snoozed items from every default view; klaxon.list excludes them by default unless include_snoozed: true is passed.

klaxon.unsnooze { id } clears snoozed_until immediately. Symmetric with klaxon.snooze — does not change status. If the item was dismissed before being snoozed, it's still dismissed after unsnooze.

Both tools return { ok: true } (snooze additionally returns the until timestamp for confirmation).

klaxon.template_save / klaxon.template_list / klaxon.template_delete

Server-stored reusable form schemas. An agent that wants to send the same form repeatedly (deployment approval, code review, incident triage) saves it once and references it from klaxon.ask by name instead of inlining the full JSON.

json
// Save (or update — version bumps automatically)
{ "name": "DeploymentApproval", "form": { /* full KlaxonForm */ } }

klaxon.template_save returns { name, version, created_at, updated_at } in content[0].text.

klaxon.template_list returns { templates: [...] } alphabetically by name.

klaxon.template_delete returns { deleted: bool, name }.

To use a saved template in klaxon.ask, pass template: "DeploymentApproval" instead of the inline form field. The two are mutually exclusive — passing both is an error.

json
{
  "level": "info",
  "title": "Approve PR #142",
  "message": "...",
  "template": "DeploymentApproval"
}

A user-facing template editor lives at Settings → Templates in the main window for editing schemas without having to call MCP tools directly.

klaxon.ack

Acknowledge a notification. Removes it from the open list (unless it has an unanswered form).

json
{ "id": "<uuid>" }

klaxon.dismiss

Dismiss a notification. Always removes it from the open list.

json
{ "id": "<uuid>" }

klaxon.search (Bundle D)

Full-text search across title and message for every item. Excludes archived and withdrawn items by default. Returns a JSON-encoded { items, count, limit, offset } object inside content[0].text.

json
{ "query": "deploy", "limit": 50, "offset": 0 }

limit defaults to 200, max 1000. offset defaults to 0. Empty queries return an empty result set — use klaxon.list to enumerate everything. Useful for de-duplication ("does this notification already exist?") and intelligent suppression workflows.

klaxon.restore (Bundle D)

Reverse klaxon.dismiss: flip a dismissed item back to open. Only operates on items currently in dismissed status — calling on any other status is a no-op (returns { ok: false, restored: false }).

json
{ "id": "<uuid>" }

Distinct from klaxon.withdraw which is the agent's "I retract this" path. Use klaxon.restore when an item was dismissed by mistake or when an agent wants to un-dismiss something it previously cleared.

klaxon.tag_add / klaxon.tag_remove (Bundle D)

Ergonomic shortcuts to mutate an item's tag list without re-sending the entire array via klaxon.update. Set semantics: duplicates on add are silently collapsed, missing tags on remove are silently skipped. Tag labels go through the same validate_label whitespace check as the creation path.

json
// klaxon.tag_add
{ "id": "<uuid>", "tags": ["escalated", "auto-retry"] }

// klaxon.tag_remove
{ "id": "<uuid>", "tags": ["auto-retry"] }

Both return { id, tags } (the updated tag list) inside content[0].text. Pass an empty array to no-op. Use klaxon.update { tags: [] } to clear all tags at once.

Symmetric "see also" ergonomic shortcuts. Same set semantics as the tag pair. UUIDs are NOT validated against existing items — dangling references are accepted (matches the creation-time contract).

json
// klaxon.related_to_add
{ "id": "<uuid>", "related_to": ["abc123-...", "def456-..."] }

// klaxon.related_to_remove
{ "id": "<uuid>", "related_to": ["abc123-..."] }

Both return { id, related_to } inside content[0].text.

klaxon.assign (Bundle E)

Set or clear an item's assignee_id. Distinct from agent_id which captures who CREATED the item — assignee is who's responsible for ACTING on it. The item still belongs to its original creator; assignee is purely a routing flag the user / triage workflow can filter on.

json
// Assign
{ "id": "<uuid>", "assignee": "agent:bot-bravo" }

// Clear
{ "id": "<uuid>", "assignee": null }

The assignee value follows the audit actor convention: user, agent:<client_id>, etc. Pass an empty string to clear (treated as null). Returns { id, assignee_id } inside content[0].text.

The main window's sidebar has an "Assigned to me" filter view that surfaces items currently assigned to user. The MCP klaxon.list tool gains a matching assignee_id filter parameter so agents can query for items assigned to anyone.

klaxon.comment_add / klaxon.comment_list (Bundle E)

Append-only comment thread on a single item. Comments are stored in the dedicated klaxon_comments table — distinct from parent_id (which spawns a sibling klaxon item). Use comments when a second agent wants to annotate someone else's klaxon ("I also see this", "fixed in PR #142") without polluting the item list.

json
// klaxon.comment_add
{ "id": "<uuid>", "text": "I also see this on staging" }

The author is captured automatically from the calling agent's x-client-id header (formatted as agent:<client_id>). For UI-driven or unauthenticated calls the author defaults to user or agent:unknown matching the actor convention used by the audit log.

Comments are append-only — no edit, no delete. If editing is ever needed it'll come as a separate migration with updated_at. klaxon.comment_add returns the persisted comment with its server-assigned id, created_at, and author. The parent item's updated_at is not bumped — the "Recently active" sort uses the item's own timestamps; comments are surfaced via the dedicated resource.

json
// klaxon.comment_list returns
{
  "comments": [
    { "id": "...", "item_id": "...", "author": "user", "text": "first", "created_at": "..." },
    { "id": "...", "item_id": "...", "author": "agent:bot", "text": "I agree", "created_at": "..." }
  ],
  "count": 2
}

Capped at 500 per item. The klaxon/item/{id}/comments resource returns the same data when you'd rather use the resource API.


Resources

The resources/list method returns these URIs; resources/read accepts the templated forms and returns the corresponding JSON payload.

URIDescription
klaxon/openEvery currently-open klaxon item (JSON array). Excludes archived/withdrawn.
klaxon/item/{id}Single item by UUID.
klaxon/answer/{id}Form response for an item, or null if unanswered.
klaxon/agentsPersisted MCP agents from the mcp_agents table — first/last seen, call counts, last tool.
klaxon/agent/{client_id}Bundle D — Single agent by client_id. Lets an agent introspect its own usage without parsing the full agents array.
klaxon/auditRecent mutations across every item (paginated). Each row has actor, action, timestamp, details JSON.
klaxon/audit/{id}Audit trail for a specific item — every mutation that touched it.
klaxon/thread/{id}Full thread for an item: ancestors via parent_id, the item itself, and all descendants.
klaxon/templatesEvery saved form template (klaxon_templates table).
klaxon/template/{name}Single template by name.
klaxon/template-preview/{name}Bundle D — Template + a validation report against an empty response. Lets agents test template hydration before calling klaxon.ask { template: "..." }. Returns { name, version, form, validation_errors, exists }.
klaxon/statsBundle D — Aggregate counts: { total, archived, snoozed, withdrawn, by_level, by_status, by_channel, by_agent }. One round-trip instead of klaxon.list + client-side reduce. Useful for smart suppression decisions ("don't post if there are already > 10 open warnings").
klaxon/deadlinesBundle D — Items with deadline_at set, sorted by proximity (soonest first). Excludes archived, withdrawn, answered, expired, and currently-snoozed items. Optional ?within=<duration> query restricts the result.
klaxon/item/{id}/commentsBundle E — Every comment on an item, oldest first. Returns the same payload as klaxon.comment_list but as a resource read.

klaxon/audit filter parameters

The audit resource accepts query-style filters via the URI fragment:

  • klaxon/audit?item_id=<uuid>&limit=100
  • klaxon/audit?actor=agent:claude-code&limit=50
  • klaxon/audit?action=create

Default limit is 100, max 1000.

klaxon/deadlines filter parameters

The deadlines resource accepts an optional ?within= query string:

  • klaxon/deadlines?within=1h — only deadlines within 1 hour
  • klaxon/deadlines?within=30m — only deadlines within 30 minutes
  • klaxon/deadlines?within=45s — only deadlines within 45 seconds
  • klaxon/deadlines?within=2d — only deadlines within 2 days
  • klaxon/deadlines?within_ms=7200000 — canonical millisecond form

Without a within parameter, every item with a deadline is returned (capped at 500). The optional shortcuts s/m/h/d are the only supported units; anything else is silently ignored.


SSE Notifications

The GET /mcp endpoint streams JSON-RPC notifications. Five type values are emitted:

json
{"jsonrpc":"2.0","method":"notifications/klaxon","params":{"type":"created","item":{...}}}
{"jsonrpc":"2.0","method":"notifications/klaxon","params":{"type":"updated","item":{...}}}
{"jsonrpc":"2.0","method":"notifications/klaxon","params":{"type":"answered","id":"...","response":{...}}}
{"jsonrpc":"2.0","method":"notifications/klaxon","params":{"type":"bulk_updated","action":"dismiss","ids":["...","..."]}}
{"jsonrpc":"2.0","method":"notifications/klaxon","params":{"type":"viewed","id":"...","viewed_at":"2026-04-08T16:32:01Z"}}
TypeWhen fired
createdA new klaxon item was created via any tool.
updatedThe item changed: status flip, message edit, archive, snooze, withdraw, etc. Fires for every mutation.
answeredThe user submitted a form response. Fires alongside updated.
bulk_updatedOnce per klaxon.dismiss_many / archive_many / mark_viewed_many call, regardless of how many items were affected.
viewedFired exactly once per item, the first time the user opens it in the main window. Subsequent views are no-ops.

updated and viewed both fire on the first view — viewed is the cleaner signal for "did the on-call see this yet?" workflows; updated is the catch-all for clients that just call refresh on any mutation.


KlaxonItem data shape

Every tool that returns an item, every resource that yields one, and every SSE created/updated event delivers the same KlaxonItem shape:

FieldTypeDescription
idUUID stringServer-assigned
levelenuminfo, warning, error, success. Visual severity, not workflow urgency — use priority for that.
titlestringRequired.
messagestring
created_atRFC3339Server-assigned
updated_atRFC3339Auto-bumped by a SQL trigger on every mutation. Drives the "Recently active" sort.
statusenumopen, answered, dismissed, expired, withdrawn
ttl_msnumber | nullAuto-dismiss TTL in milliseconds.
formKlaxonForm | nullForm schema (if any).
actionsKlaxonAction[]Action buttons (default includes a single Ack action).
responseobject | nullForm response after the user submits.
answered_atRFC3339 | nullWhen the user submitted.
channelstring | nullSidebar grouping label.
tagsstring[]Free-form labels.
archived_atRFC3339 | nullWhen archived (hidden from default views). Distinct from dismissed.
viewed_atRFC3339 | nullFirst-view timestamp. null = the item is still "new" and counts toward the unviewed badge.
agent_idstring | nullThe MCP x-client-id of the agent that created the item. null for Tauri-command-driven creates.
parent_idUUID | nullThread parent.
related_toUUID[]Symmetric "see also" links.
priorityintegerWorkflow urgency. 0 unset, 1 low, 2 medium, 3 high, 4 urgent. Distinct from level.
deadline_atRFC3339 | nullUser-facing "must answer by" timestamp. Distinct from the auto-dismiss ttl_ms.
metadataobjectFree-form key/value bag (correlation_id, deploy_sha, jira_ticket).
snoozed_untilRFC3339 | nullHidden from default views until this timestamp passes; a 60s background ticker clears it.
assignee_idstring | nullBundle E Phase 2 — who's responsible for ACTING on this item. Distinct from agent_id (creator). Same actor convention: user, agent:<client_id>. Settable via klaxon.assign or via the main window's UI.

Lifecycle states

              ┌─────────┐
              │   open  │  ← created by notify / ask / confirm / choose
              └─────────┘

   ┌───────────────┼───────────────┐
   │               │               │
   ↓               ↓               ↓
answered       dismissed       expired
   │               │               (TTL elapsed; lazy on read)
   │               │
   └──────┬────────┘

      withdrawn  ← agent-driven retraction (status flag, not separate state)


       archived_at IS NOT NULL  ← orthogonal "hide" flag, can apply to any non-open state

snoozed_until is also orthogonal — a snoozed item can be in any status, but the UI hides it from default views until the deadline passes.


Form Field Types

Forms support these field types in the fields array:

TypeDescriptionType-specific propertiesResponse value
textSingle-line text inputplaceholder, default, min_len, max_len, patternstring
textareaMulti-line text inputplaceholder, default, min_len, max_len, patternstring
numberNumeric inputdefault, min, maxnumber
selectDropdown selectoptions: [{value, label?}], defaultstring (must be in options)
multiselectMulti-select dropdownoptions, defaultstring[] (each in options)
radioRadio button groupoptions, defaultstring (must be in options)
checkboxSingle checkboxdefaultboolean
toggleToggle switch (on/off slider)defaultboolean
yesnoYes/No pill buttons. Prefer this over toggle for binary questionstoggle reads as on/off, not yes/no.default, yes_label, no_labelboolean
datetimeDate/time pickerdefaultstring (ISO 8601)
issuepickerIssue ID input with autocompleteplaceholder, default, suggestions: string[]string
diffapprovalDiff viewer with Approve / Reject buttonsdiff (required), approve_label, reject_label"approve" or "reject"
ratingStar ratingmin (default 1), max (default 5), defaultnumber (integer in [min, max])
sliderRange slidermin (required), max (required), step (default 1), defaultnumber (in [min, max])
markdownRead-only markdown contentcontent (required)no value — read-only
fileuploadFile / image upload (base64 transport)accept (MIME list, supports image/*), max_bytes{ filename, mime, size, data } (data is base64)
taginputFree-form chip input. Type and press Enter or comma to commit; backspace removes the previous chip.suggestions: string[], placeholder, maxstring[]
repeatRepeating group of sub-fields (array of sub-forms). UI renders an [+ Add row] button.fields: FormField[], min, maxArray<{ <inner field id>: <inner value> }>

Common properties

All field types (except markdown) support these properties:

PropertyTypeDescription
idstringRequired. Unique field identifier — used as the response key.
labelstringRequired for non-markdown. Display label.
requiredbooleanWhether the field must have a value before submit.
helpstringHelp text shown under the field.

Option shorthand

For select, multiselect, and radio, the options array accepts either a string shorthand or full {value, label} objects. The two forms can be mixed:

json
"options": ["red", "blue", { "value": "g", "label": "Green" }]

The shorthand "red" is equivalent to { "value": "red" }.

Server-side validation

When an answer is submitted via the UI, the Rust backend validates the response against the form schema before persisting. The following are enforced:

  • Required fields: must have a non-empty value
  • text / textarea: min_len, max_len, pattern (regex)
  • number: must be a number; min, max
  • rating: must be an integer; min, max
  • slider: must be a number; min, max
  • select / radio: value must be in options
  • multiselect: every value must be in options
  • checkbox / toggle / yesno: must be a boolean
  • diffapproval: must be "approve" or "reject"
  • datetime / issuepicker: must be a string
  • fileupload: sizemax_bytes; mime matches accept (literal or prefix/* wildcards)
  • taginput: array of strings; length ≤ max
  • repeat: array of objects; min ≤ length ≤ max; recursively validates every inner field per row (errors include the row index, e.g. field 'items'[2].name is required)

If validation fails, klaxon_answer returns an error and the response is not persisted. The main window's detail pane surfaces the error message in a red banner above the form so the user can see exactly what went wrong.

Multi-page conditional branching

For multi-page forms with conditional branches in next, the validator only walks pages the user actually visited based on their response. The traversal mirrors the JavaScript wizard's handleNext exactly: start at the first page, follow Fixed links, evaluate Conditional branches against the response values (booleans/numbers/strings stringified the same way the wizard does), terminate on End or an unmatched conditional without a default, and skip every page not on the chosen path.

This means a form with a conditional like:

json
{
  "kind": "conditional",
  "field_id": "all_good",
  "branches": [
    { "value": "false", "page_id": "deep_dive" },
    { "value": "true",  "page_id": "wrap" }
  ],
  "default": "wrap"
}

…lets the user submit when all_good = true even if the deep_dive page has required fields the user never filled — the validator recognizes that page is unreachable and skips it. The walk is cycle-safe via a visited set.