---
name: applicaster-forms-api
description: "Form Screen (Forms API): use when building a Zapp Form Screen — profile create/edit, account details, any data-entry screen — or the backend behind it. Covers the form JSON configuration (properties, field types, presets, options, tap_actions), the feed entry that prefills it via extensions.form_data, the postForm CloudEvent the app submits, and the formError/fieldErrors response that surfaces server validation inline. Works in any backend stack."
---

# applicaster-forms-api

Build a **Zapp Form Screen** and/or the backend behind it. The Form Screen plugin renders a
data-entry screen from a JSON **form configuration**, prefills it from the selected feed
entry's `extensions.form_data`, and submits the screen state as a CloudEvent via the
`postForm` action. The deliverable is a form whose configuration, prefill feed, and endpoint
agree on **field IDs**, with validation on **both** the request boundary and the response
boundary, plus an executable proof via the skill-local [Form E2E tool](tools/form-e2e/README.md).

Three artifacts, one contract:

1. **Form configuration feed** — `properties[]`, one per field. Defines what is rendered.
2. **Entry feed** (`extensions.form_data`) — prefill values for edit; absent or `{}` for create.
3. **Backend** — receives `data.form_data` from the `postForm` CloudEvent, returns `formError` / `fieldErrors` keyed by the same field IDs.

For general Cloud Events receiver work (multi-event dispatch, allowlists, idempotency across
the full protocol), use [applicaster-cloud-events](../applicaster-cloud-events/SKILL.md). This
skill covers the **form boundary** only.

The live [Forms API guide](https://docs.applicaster.com/integrations/forms-api) is the
wire-contract authority. Field type definitions are in [formTypes.ts](references/formTypes.ts) —
the TypeScript source the plugin is written against.

## Scope

- Use when authoring a form configuration JSON, an editable-entries feed carrying `form_data`, or the endpoint that receives `postForm` submissions.
- **Adapt to the target project's stack** — language, framework, validation library, HTTP client, test runner, config/secrets, and deployment conventions. Do not assume a JavaScript implementation.
- Out of scope: the profile picker feed itself (see the live [Profiles Feed](https://docs.applicaster.com/integrations/profiles-feed) guide) and Zapp screen/plugin setup beyond the handoff section below.
- The contract is fixed and documented. Do not invent field types, option keys, action types, or response fields.

## Non-Negotiable Gates

1. **Field IDs are the contract.** The same string appears in `properties[].id`, prefill `form_data`, submitted `data.form_data`, and `fieldErrors`. Never key errors by preset, component, or UI id.
2. **The component vocabulary is closed.** Every field's `type`, `preset`, `options.inputType`, and every tap action `type` must be one [formTypes.ts](references/formTypes.ts) declares, or a recorded exception — today that is the `goBack` action, supported by the plugin ahead of the types and carried in `types.js` as `PENDING_ACTION_TYPES`. A preset outside that set — including the guide's `FormButtonSave` and custom presets such as `FormAvatarPicker` — is used only after it is confirmed against the installed plugin and recorded. `form-e2e` enforces this.
3. Every submission validates its **request** — envelope, `data.form_data`, and per-field shapes — before doing any work, and validates its **response** before returning it. Client-side `validateForm` is a UX affordance, never enforcement. See [validation-patterns.md](references/validation-patterns.md).
4. **Multiselect crosses the wire as `string[]` of option `const` values**, both directions. Empty selection is `[]` — never `""`, never omitted, never an array of option objects. Option `const` values must not contain commas.
5. Server validation failures return a **failed HTTP status** carrying `formError`, `fieldErrors`, or both. A 2xx with an error body is read as success and the rest of the button's action chain runs.
6. **Preserve hidden keys.** Any `form_data` key without a matching `properties[]` entry stays in screen state and returns on submit — that is how immutable identifiers such as `profileId` survive a round trip. Never blind-write or silently strip them.
7. Never log raw `form_data`; it carries user PII. Logs carry only field IDs, operation, and status class.
8. Do not report the work complete without a `## Zapp Configuration` section wiring the Form Screen plugin, the Form Feed, and the entry type mapping.

## 1. Establish facts

1. Read [forms-contract.md](references/forms-contract.md), [validation-patterns.md](references/validation-patterns.md), [decision-guidance.md](references/decision-guidance.md), and [formTypes.ts](references/formTypes.ts).
2. Fetch the live [Forms API](https://docs.applicaster.com/integrations/forms-api) guide, the [Cloud Events](https://docs.applicaster.com/integrations/cloud-events) guide for envelope handling, and [Profiles Feed](https://docs.applicaster.com/integrations/profiles-feed) if this is a profile form. Reconcile drift toward live docs.
3. Inspect the target project and determine, without asking: stack, existing Cloud Events routes, validation approach, test/build commands, endpoint URL shape, and where the entity being edited is stored. Only ask for details the project cannot answer yet.
4. Establish the field list and, per field, whether it is **editable**, **hidden but round-tripped**, or **server-owned**.

If live documentation cannot be fetched, state that the bundled snapshot may be stale and ask whether to proceed from [forms-contract.md](references/forms-contract.md).

Completion: the field list, editability per field, and the target stack are evidenced by code and live docs.

## 2. Resolve developer decisions

Present the fact summary, then ask only what the project and documentation cannot settle.
**One question at a time**, each carrying the discovered constraint, a recommended answer, and
the consequence of choosing otherwise. If a question exposes a missing fact, go investigate and
come back with a recommendation rather than asking the developer to decide blind. Use
[decision-guidance.md](references/decision-guidance.md) for the full branch rules. Resolve in
this dependency order:

Ask first, because the answer decides where the field IDs come from:

1. **Which artifacts do you own here — the form configuration, the backend, or both?**

   - **Both** — the usual case for a new editable entity. Fix the field ID list once, then derive config, prefill, and endpoint from it.
   - **Backend only** — the form config is already live or owned by another team. Fetch it now; field IDs come from `properties[].id`, never from your entity model. Renaming a field server-side silently breaks prefill and `fieldErrors`.
   - **Form config only** — the endpoint exists. Get its accepted payload before authoring, and verify against it in step 5.

Then, once scope is fixed:

2. **What is the user editing, and what is the complete field list?** For **each** field, one of
   three: **editable** (gets a `properties[]` entry), **hidden but round-tripped** (in `form_data`
   only, resubmitted unchanged — the supported way to carry `profileId`), or **server-owned**
   (absent from `form_data`, derived from the session). Recommend server-owned for anything the
   user must not change; a server-owned value placed in `form_data` becomes client-mutable.

3. **Which component renders each field?** Walk the field list and have the developer name a
   `type` and `preset` per field from the set [formTypes.ts](references/formTypes.ts) declares —
   do not infer them from the data model. Offer the palette and the trade-off:

   | Want | `type` | `preset` |
   | --- | --- | --- |
   | Free text, a name, an email | `textInput` | `FormTextInput` (`options.inputType: "number"` for numeric, `secure: true` to mask) |
   | A date | `datePicker` | `FormTextInput` — the contract is `datePicker`; it renders through the text input for now |
   | A yes/no toggle | `checkBox` | `FormMultiSelectGroup` |
   | Pick exactly one | `singleSelect` | `FormMultiSelectGroup`, or a custom preset such as `FormAvatarPicker` for a visual picker |
   | Pick several | `multiSelect` | `FormMultiSelectGroup` |
   | Static explanatory text | `label` | `""` — confirm plugin support first |
   | Save, cancel, delete | `button` | `FormButton` / `FormButtonCancel` / `FormButtonDelete` |

   Record every preset outside that set, and why it was accepted — `form-e2e` will fail on it
   until it is passed through `--allow-presets`. A preset the installed plugin cannot resolve
   renders nothing.

4. **Is create in scope, and how does the user reach it?** An extra entry appended to the
   editable feed (often with a distinct type such as `profile-add`), or a separate Zapp Manual
   Feed behind a "Create New" button. Either way the entry must resolve to the Form Screen with
   `form_data` omitted or `{}`. Without such an entry there is no create path — edit-only is a
   valid answer, but make it deliberate.

5. **For each select field, where do the options come from — inline `items` or `itemsFeedURL`,
   and who owns that feed?** Recommend inline `items` for small static sets and `itemsFeedURL`
   for dynamic, localized, or externally owned sets. `formTypes.ts` requires `items` on
   `singleSelect`; confirm the installed plugin accepts `itemsFeedURL` alone before dropping
   inline items. Confirm every option `const` is comma-free.

6. **Which buttons does the form need — save, cancel, delete — and what does each one do?**
   The preset is the role and the styling; the behaviour is entirely in `tap_actions`, so name
   both per button. Worked JSON for all three is in
   [forms-contract.md](references/forms-contract.md#buttons):

   | Role | Preset | Chain | Why that order |
   | --- | --- | --- | --- |
   | Save | `FormButton` (or `FormButtonSave`, if confirmed) | `validateForm` → `postForm` → `navigateToScreen` | Validate before the request; navigate last so a rejected save leaves the user on the form with the errors showing |
   | Cancel | `FormButtonCancel` | `navigateToScreen`, optionally after `confirmDialog` | No `validateForm` — a half-filled form is what the user is abandoning |
   | Delete | `FormButtonDelete` | `confirmDialog` → `sendCloudEvent` (or `postForm`) → `navigateToScreen` or `goBack` | Deletion is not undoable, so confirm first; nothing to validate and nothing for the server to refuse, so `sendCloudEvent` is enough — use `postForm` only if a refusal must reach the user |

   Confirm each destination `typeMapping`, and a distinct `subject` per write so the backend can
   tell save from delete on a shared endpoint. Order matters: the chain stops when the server
   returns field errors, so anything that must not run on failure sits after `postForm`.

7. **Which constraints are client-side hints, and which are business rules only the server can
   decide?** `required`, `minLength`/`maxLength`, `minItems`/`maxItems` render inline before
   submission; uniqueness, profanity, age gates, and entitlement checks come back as
   `fieldErrors`. Re-check every client constraint server-side regardless — `validateForm` is a
   UX affordance, not enforcement. Duplicating a business rule in the config produces two
   messages that drift apart.

8. **What does the endpoint do with `form_data` keys it does not recognize — persist, ignore, or
   reject?** Recommend an explicit allowlist of writable field IDs. Blind-writing unknown keys
   lets a crafted payload set server-owned columns; blind-stripping them drops the hidden
   identifiers the form was relying on to round-trip.

9. **Which reading of the ambiguous parts of the contract does the installed Form Screen plugin
   implement?** Button preset (`FormButtonSave` vs `FormButton` / `FormButtonCancel` /
   `FormButtonDelete`), whether `tap_actions` sit under `options.extensions` or field-level
   `extensions`, and whether `label` is supported. `sendCloudEvent`, `confirmDialog` and
   `goBack` are confirmed usable — no need to ask. Check the installed plugin version first; ask only what
   inspection cannot settle. Do not guess — a wrong preset renders no button.

10. **What are the target project's response-validation convention and the runnable `form-e2e`
   inputs?** Form config URL or file, entry feed URL, endpoint URL, a disposable record safe to
   write, and which field the invalid-payload scenario should break (`--invalid-field`).

Completion: every applicable decision has a recorded developer answer, including the
`type`/`preset` chosen per field, the full editable/hidden/server-owned ledger, and the
unknown-key policy.

## 3. Confirm before edits

Present a concise contract containing:

- The field table: id, `type`, `preset`, required/constraints, prefill shape, submitted shape — with any preset outside [formTypes.ts](references/formTypes.ts) flagged and justified.
- Hidden keys carried through `form_data`, and the policy for unknown keys server-side.
- Each button: role, preset, action chain in order, destination `typeMapping`, and the `postForm` `url`, `type`, and `subject` it uses.
- Success and error responses, including a sample `fieldErrors` body.
- Affected routes/files, tests to add or update, and configuration required.

Wait for explicit approval.

Completion: the developer has approved every non-discoverable choice.

## 4. Implement the contract

1. Author the form configuration feed using only the types, presets, options, and action types [formTypes.ts](references/formTypes.ts) declares, and the chain order per button role from [forms-contract.md](references/forms-contract.md#buttons) — every property has `id`, `type`, `preset`, and `options.title`; select fields declare `items` or `itemsFeedURL`.
2. Emit prefill `form_data` using the server value shapes in [forms-contract.md](references/forms-contract.md). Omit `form_data` or send `{}` for create. Do not prefill `null` into visible fields.
3. Validate the request boundary before any store or provider work: envelope, `data.form_data`, then per-field shapes and constraints.
4. Read values from `data.form_data`, normalizing per field type — tolerate `"true"` / `"false"` for checkboxes, treat a missing multiselect as `[]`.
5. Return `fieldErrors[fieldId] = string[]` with a failed status on validation failure; add `formError` for form-level messages.
6. Ensure no route logs raw `form_data`. Make only the direct changes the approved contract requires.

Completion: code matches the approved contract.

## 5. Verify and hand off

1. Run the target project's unit/integration tests for the form route.
2. Run the skill-local `form-e2e` tool against a running endpoint with a disposable record:

   ```sh
   node <skill-tools-dir>/form-e2e/cli.js \
     --form-config-url https://feeds.example.com/profile-form.json \
     --entry-feed-url https://feeds.example.com/editable-profiles.json \
     --endpoint-url https://api.example.com/events
   ```

   Add `--allow-presets` for any preset step 2 confirmed beyond what `formTypes.ts` declares.

   Use `--skip-submit` for a config-and-prefill-only pass before the endpoint exists.
3. Walk [acceptance-matrix.md](references/acceptance-matrix.md), marking N/A for field types the form does not use.
4. Report any failures with sanitized evidence: attach the `--json-report` output, which redacts every value the endpoint returned. Never paste a raw response body or raw `form_data`.
5. Produce the Zapp Configuration handoff below.

Completion: tests, `form-e2e`, and the acceptance matrix pass, or blockers are named.

## Zapp Configuration

Fetch current documentation before producing this section. Include:

1. Add the **Form Screen** plugin to the app and create a screen of that type.
2. In the screen's **Data** section, assign the form configuration feed to the **Form Feed** selector.
3. Map the entry `type.value` (for example `profile-edit`, and `profile-add` for create) to that Form Screen in the app's type mapping, so tapping an entry navigates to it and passes the entry data.
4. Attach the editable-entries feed to the list screen; attach a separate manual feed (one entry with `form_data: {}`) to any "Create New" button.
5. Point the `postForm` action's `url` at `<DEPLOYED_FORM_ENDPOINT_URL>`, with the agreed `type` and `subject`.
6. Ordered validation: edit an entity, create one, trigger a server validation error, confirm the navigation action after `postForm` does not run, confirm hidden identifiers round-trip.
7. `Documentation checked: YYYY-MM-DD` with links to the guides used.
