# Field keys

The stable names automations use to address your fields — where they come from, and how to keep them stable.

## Field keys

A field key is the developer-facing name of one field in one form — company_name, contacts. It is how an automation prefills a field, locks it, or reads the answer back, and it survives retitling and duplicating.

<h2 id="why">Why they exist</h2>

<p>
  Without field keys, an automation has to address your questions by internal ids that mean nothing to anyone — and a webhook payload
  arrives full of them. With field keys, both sides read the same way:
</p>

```
{ "company_name": "Acme", "employees": 120, "contacts": [{ "name": "Ada" }] }
```

<p>
  Field keys are used in two places: <code>prefill</code>, <code>context</code>, and <code>readonly</code> when creating a request; and the{' '}
  <code>answers</code> and <code>display</code> maps in every callback and every webhook payload.
</p>

<h2 id="where">Where to find them</h2>

<p>
  Every key the form publishes lives in one place, the <strong>Keys</strong> table: each field, and under a choice question each option,
  under a matrix each row and column. It ends with a preview of the <code>answers</code> object your callback will carry, with your keys in
  it.
</p>

<p>
  Each option's hint row — the grey line under the option you are editing — ends with its key as a chip. Click the chip to edit the key in
  place, press Enter or Escape when done. A matrix shows the chip for the row or column whose label you are editing. A chip turns amber when
  the form is published and the key you typed would move the one automations already use.
</p>

<p>
  The same table appears read-only where you wire an integration: under the webhook's field mapping, and under the request snippets in the
  Share sheet's Requests card. You can also read every key at once with <code>fields.list</code>.
</p>

<h2 id="derivation">How a key is derived</h2>

<p>
  You do not have to set anything. Until you edit it, a key is derived from the question's title: accents are folded and letters such as ø,
  æ and ß become o, ae and ss, everything is lowercased, each run of anything that is not a letter or digit becomes a single underscore,
  leading and trailing underscores are dropped, and the result is cut to 64 characters.
</p>

<p>
  A title written only in a non-Latin script, such as Arabic, Hebrew, Cyrillic, Greek, Chinese or Japanese, has nothing to fold, so its key
  is <code>field_</code> plus its position, and an option's is <code>option_</code> plus its position. These keys are as stable as any other
  once published, but they say nothing about the question. When an automation reads the answers, set a readable key yourself in the Keys
  table.
</p>

<p>
  If two fields would end up with the same key, formbase breaks the tie in document order by appending <code>_2</code>, <code>_3</code>, and
  so on. A key you set yourself always wins its claim, and the derived one yields.
</p>

<h2 id="setting">Setting your own key</h2>

<p>
  Type a name in the Field key input to override the derived one. Clearing the input goes back to the derived key. Allowed characters are
  letters, digits, <code>_</code>, <code>.</code> and <code>-</code>, up to 64 characters — anything else is refused with{' '}
  <em>"Use only letters, numbers and _ . - (max 64 characters)."</em> Spaces are turned into underscores as you type, so "contact name"
  becomes <code>contact_name</code>.
</p>

<p>
  Keys are case-sensitive and must be unique within one form. Reusing one already taken by another question, repeating group, hidden field,
  or calculated field is refused with <em>"Another field already uses this key."</em>
</p>

<h2 id="freeze">Keys freeze at first publish</h2>

> ⚠️ **Renaming a published key breaks automations**
> <p>
>     On a published form, the Field key input warns you that the form is published and automations using the current key will break. Nothing
>     stops you, but every workflow that prefills or reads that key stops matching the moment you publish the change. Update the automation in
>     the same sitting. Publish <a href="#removed-keys">warns you again</a> before the change goes live.
>   </p>

<p>
  The first time you publish, every field's key is written into that published version. Every later publish carries the same keys forward,
  which means:
</p>

<ul>
  <li>
    <strong>Retitling never moves a key.</strong> Rename "Company name" to "Legal entity name" and the key stays <code>company_name</code>.
    Your automations keep working; only the words on the page change.
  </li>
  <li>
    <strong>Changing a question's type never moves a key.</strong> Turning a text question into a dropdown keeps its key — though the value
    shape your automation must send changes with it.
  </li>
  <li>
    <strong>Moving a question never moves its key.</strong> Position only matters for the positional fallback on a field that has no usable
    title.
  </li>
  <li>
    <strong>Duplicating a block gives the copy a new key.</strong> A key you set yourself is copied and re-keyed to the next free{' '}
    <code>_2</code>; derived keys are made unique at publish.
  </li>
  <li>
    <strong>Forms built before field keys existed</strong> get theirs at their next publish.
  </li>
</ul>

<p>Publishing is also where key collisions are caught, and both of them stop the publish rather than silently renaming a field:</p>

<ul>
  <li>
    Two fields claiming one key — <em>Field key "…" is used by more than one field.</em>
  </li>
  <li>
    A key you typed onto one field that another field already published —{' '}
    <em>
      Field key "…" is already published on "…". Giving it to "…" would rename that field's key to "…_2" and break automations using "…".
    </em>{' '}
    Free the key on one of the two and publish again.
  </li>
</ul>

<p>
  Both show up beside the Publish button with every other pre-flight finding — see{' '}
  <a href="/building-forms/publish-checks">Publish checks</a>.
</p>

<h2 id="removed-keys">When a published key is about to disappear</h2>

<p>
  A key belongs to the field it was published on, not to its title. So there are two ways to lose one without meaning to: type a different
  key on a published field, or <strong>delete a question and add a new one in its place</strong>. The new question is a new field — it gets
  a fresh key derived from its own title, and the old key is gone.
</p>

<p>
  Nothing fails on formbase's side when that happens. The webhook still fires and the callback still arrives, only without that answer, and{' '}
  <code>requests.create</code> starts refusing the old key with <code>UNKNOWN_FIELD_KEY</code>. So publish checks for it first. When a key
  the live version publishes would not exist in the next one, the publish dialog and the issue indicator beside the Publish button show a
  warning:
</p>

```
Field key "company_name" will no longer exist after this publish. Integrations and requests that use it will stop receiving that answer. A new field "Company" publishes as "company". Set its field key to "company_name" to keep them working.
```

<ul>
  <li>
    <strong>To keep your automations working</strong>, open the <strong>Keys</strong> table, find the field the warning names, and type the
    old key. The warning clears and the key carries on as if nothing happened.
  </li>
  <li>
    <strong>If you removed the field on purpose</strong>, publish anyway — it is a warning, not an error — and update the automations that
    read the key.
  </li>
  <li>
    The warning names a field only when the choice is obvious: the field whose key you retyped, or the single new field standing where a
    single deleted one was. Otherwise it names just the key.
  </li>
  <li>
    Deleting a repeating group warns for the group's key and for the key of every field inside it. Publishing through the MCP server's{' '}
    <code>form_publish</code> returns the same messages in <code>warnings</code>.
  </li>
  <li>
    Option, row and column keys get the same treatment: delete an option or retype its key on a published form and publish warns{' '}
    <em>Option key "pro" of "Plan" will no longer exist after this publish</em>, naming the key the option publishes as now when it still
    exists. The publish dialog lists every key change, at both levels, under <strong>Keys changing in this publish</strong>.
  </li>
</ul>

<h2 id="groups">Repeating groups and hidden fields</h2>

<p>
  A <a href="/building-forms/repeating-groups">repeating group</a> has its own key, and so does each field inside it. Automations address
  the group as a whole and nest the members:
</p>

```
{ "contacts": [{ "name": "Ada", "email": "ada@acme.com" }, { "name": "Grace", "email": "grace@acme.com" }] }
```

<p>
  A field inside a group is only reachable through its group — there is no top-level <code>name</code> here, only{' '}
  <code>contacts[0].name</code>. Rename the group's key and the whole array moves; rename one member's key and only that name inside each
  object changes.
</p>

<p>
  A <a href="/building-forms/hidden-fields">hidden field</a>'s parameter name is its field key. That is the key you put in{' '}
  <code>context</code> when creating a request, and the same name you would use in a URL parameter on a public link.
</p>

<p>
  A <a href="/building-forms/calculated-fields">calculated field</a>'s name is its field key, and it shares the form's one set of keys with
  everything else. It is read-only: the form works out its value, so you can never send one — <code>fields.list</code> lists it with{' '}
  <code>calculated: true</code> and <code>requests.create</code> refuses it in <code>prefill</code> and <code>context</code>. You do read it
  back: it arrives in <code>answers</code> under its name (<code>answers.total</code>). Renaming a published calculated field renames its
  key, with the same publish warning as any other field.
</p>

<h2 id="option-keys">Option keys: the choices inside a question</h2>

<p>
  The parts of a question that an answer names get keys too. Every option of a radio, select, checkbox, picture choice or ranking question,
  and every row and column of a matrix, has an <strong>option key</strong>: derived from its label the same way a field key is derived from
  a title, editable, and frozen at publish. So an answer reads as a name on both sides:
</p>

```
{ "plan": "pro", "interests": ["billing", "api"], "satisfaction": { "delivery_speed": "very_good" } }
```

<p>
  A workflow branches on <code>answers.plan == "pro"</code> whatever language the respondent answered in, and a request prefills a choice
  with <code>{'{ "plan": "pro" }'}</code>. The keys are listed by <code>fields.list</code> under each field's <code>options</code>, or a
  matrix's <code>rows</code> and <code>columns</code>, and edited in the <a href="#where">Keys table</a> or on the option's chip. Each
  question's options are their own namespace, so two questions may both have a <code>yes</code>, and a matrix row and column may share a
  key. Two options of one question that would derive the same key are told apart with <code>_2</code>, like fields.
</p>

<p>
  The <a href="/requests/decisions-and-approvals">decision question</a> is an ordinary radio whose three options carry the keys{' '}
  <code>approve</code>, <code>decline</code> and <code>changes</code>; that is what makes a request's <code>outcome</code> a closed set.
</p>

<div class="not-prose grid gap-3 sm:grid-cols-2">
  - [Create a request](/requests/creating-requests) — Put those keys to work.
  - [Webhook reference](/developers/webhooks-reference) — How field keys shape every event payload.
</div>
