---
title: "Public forms (PublicFormDecl)"
description: "Generated from the doc schema (CatalogDocSchema, @canonify/kernel-catalog/doc): each field’s type, default and description are the schema’s own, and each example is a bundle file that readBundle…"
order: 6
id: "model/form"
---

# Public forms (PublicFormDecl)

Generated from the doc schema (`CatalogDocSchema`, @canonify/kernel-catalog/doc): each field’s type, default and description are the schema’s own, and each example is a bundle file that readBundle decodes. A field’s Example is its value in one of those examples.

## PublicFormDecl {#publicformdecl}

A public form (runtime, C34).

The `forms` slot of the doc holds these, keyed by id. In a bundle each is one file, `forms/<name>.json`, named by its `slug`, which the file then leaves out.

| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
| `id` | [FormId](shapes.md#formid) | yes (propose assigns it; a new declaration leaves it out) | — | — | A public form id (`fm_…`); renames keep it. |
| `action` | [ActionId](shapes.md#actionid) | yes | — | `"ac_1b3d5f7h9k"` | The action a submission calls; it has no subject. |
| `slug` | [Ident](shapes.md#ident) | yes | — | `"contact"` | The form’s name in its public address. |
| `title` | [Text](shapes.md#text) | yes | — | `"Contact us"` | The title shown on the form. |
| `inputs` | [Ident](shapes.md#ident)[] | yes | — | `["name","email","message"]` | The action inputs the form shows, in field order. |
| `fixed` | Record<[Ident](shapes.md#ident), [Expr](shapes.md#expr)> | no | — | `{"source":"'web'"}` | Inputs the form sets itself: actor, clock and literal CX values only. |
| `identity` | `"anonymous"` \| `"email_pin"` | yes | — | `"email_pin"` | Who submits: anyone, or a person who proves an email address with a PIN (authz). |
| `onSubmit` | [PublicFormDecl.onSubmit](form.md#publicformdecl.onsubmit) | no | — | (see the example) | What a submission does besides the call. |
| `challenge` | `"turnstile"` | no | — | `"turnstile"` | A human check the visitor must pass before the submission is looked at; without a passed check it refuses with form.challenge_failed. |
| `silentThankYou` | `true` | no | — | `true` | The form accepts that when a rule over other rows refuses a submission (a unique key, a no_overlap, an invariant that reads past its own row), the visitor is thanked as if all were well and nothing is written or sent, so no stranger learns that a row exists. Any refusal after the action starts writing gets the same thanks, a fault in the form too, and only the audit row says why; a fail trigger on the action can tell the author. A form its action can meet such a rule on does not compile without it (form.silent_thank_you_undeclared); a form that cannot is refused for declaring it (form.silent_thank_you_unneeded). To say "taken" instead, add a precondition over a type its visitors read. |
| `open` | boolean | yes | — | `true` | Whether it accepts submissions; a closed form refuses with form.closed. |

`forms/contact.json`

```json
{
  "action": "ac_1b3d5f7h9k",
  "title": "Contact us",
  "inputs": ["name", "email", "message"],
  "fixed": { "source": "'web'" },
  "identity": "email_pin",
  "onSubmit": {
    "emit": {
      "to": "email.send_template",
      "input": { "recipient": "me.email", "template_id": "'received'" },
      "maps": { "params": { "name": "input.name" } }
    },
    "message": { "text": "Thank you. We will answer within a day." }
  },
  "challenge": "turnstile",
  "silentThankYou": true,
  "open": true
}
```

## PublicFormDecl.onSubmit {#publicformdecl.onsubmit}

What a submission does besides the call.

| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
| `emit` | [PublicFormDecl.onSubmit.emit](form.md#publicformdecl.onsubmit.emit) | no | — | (see the example) | A delivery sent with the submission, in the same transaction. |
| `message` | [MsgRef](shapes.md#msgref) | yes | — | `{"text":"Thank you. We will answer within a day."}` | What the person sees after submitting. |

## PublicFormDecl.onSubmit.emit {#publicformdecl.onsubmit.emit}

A delivery sent with the submission, in the same transaction.

| Field | Type | Required | Default | Example | Description |
|---|---|---|---|---|---|
| `to` | string (matching `^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$`) | yes | — | `"email.send_template"` | A delivery operation: the only I/O, sent after commit (e.g. webhook.post). |
| `input` | Record<string, [Expr](shapes.md#expr)> | yes | — | `{"recipient":"me.email","template_id":"'received'"}` | The CX value of each input of the delivery operation. |
| `maps` | Record<string, Record<string (matching `^[A-Za-z][A-Za-z0-9_]*$`), [Expr](shapes.md#expr)>> | no | — | `{"params":{"name":"input.name"}}` | The inputs that hold an object (e.g. params, or a webhook body): each field name with its CX value. |
