> For the complete documentation index, see [llms.txt](https://documentation.opencrvs.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://documentation.opencrvs.org/v1.9/setup/3.-installation/3.2-set-up-your-own-country-configuration/3.2.7-configure-declaration-forms/4.2.6.2-configure-an-event-declaration-form.md).

# 4.2.6.2 Configure an event declaration form

* Define the structure of the form into pages using defineDeclarationForm
* Build each page from the available form fields, validations, conditionals etc

## Define form structure using `defineDeclarationForm()`

In the v2 events engine, every event must declare its **Declaration Form** — the form a registrar completes when declaring the event (birth, death, marriage, etc.).

To make this easier and strongly typed, OpenCRVS provides the helper:

```ts
defineDeclarationForm(...)
```

This constructs a **validated, typed declaration form configuration** that can be assigned to:

```ts
declarationForm: defineDeclarationForm({...})
```

inside your `EventConfigInput`.

***

## 1. What `defineDeclarationForm()` does

`defineDeclarationForm()`:

* Ensures your declaration form follows the required v2 structure
* Guarantees consistent typing for pages, groups and fields
* Applies Zod validation so invalid form structures fail at service startup rather than in production
* Produces the exact shape the events engine expects in `EventConfigInput`

It’s essentially a **form builder** with strong compile-time and runtime validation.

***

## 2. Structure of `defineDeclarationForm()`

It accepts a single config object:

```ts
defineDeclarationForm({
  label: TranslationConfig,
  pages: PageConfig[]
})
```

Let’s break down the example:

```ts
defineDeclarationForm({
  label: {
    defaultMessage: 'Death declaration form',
    id: '',
    description: ''
  },

  pages: [deathIntroduction, deceased, eventDetails, informant, documents]
})
```

***

## 3. `label` — The translated heading of the form

```ts
label: {
  defaultMessage: 'Death declaration form',
  id: '',
  description: ''
}
```

This is a **TranslationConfig** object.

It defines the name that appears at the top of the declaration flow.

#### Important notes:

* `defaultMessage` is what appears in English (or fallback language)
* `id` should be a proper translation key (e.g. `"event.birth.declarationForm.label"`)
* `description` is for translators

This label is shown in various parts of the UI, including on page headers.

***

## 4. `pages` — The ordered list of pages in the declaration form

```ts
pages: [deathIntroduction, deceased, eventDetails, informant, documents]
```

The array contains **Page Config Objects**, each representing a “page” of the declaration form.

#### A **Page** represents:

* One screen in the UI
* Containing one or more **groups**
* Each group containing one or more **fields**

Pages are defined separately (e.g. in `deathIntroduction.ts`, `deceased.ts`, etc.) so you can re-use them or swap them between events.

#### Why the order matters

The order you put the pages in this array determines how the registrar navigates the form:

1. `deathIntroduction`
2. `deceased`
3. `eventDetails`
4. `informant`
5. `documents`

The form wizard/navigation follows this order exactly.

***

## 5. What a page looks like

A page config typically looks like:

```ts
export const deceased = definePage({
  id: 'deceased',
  label: { id: '...', defaultMessage: 'About the deceased' },
  groups: [
    defineGroup({
      id: 'identity',
      label: { ... },
      fields: [ firstName, lastName, sex, dateOfBirth ]
    }),
    defineGroup({
      id: 'documentation',
      fields: [ nationalId, passportNumber ]
    })
  ]
})
```

`defineDeclarationForm()` collects all pages and produces the full declaration form schema.

***

## Build each page from the available form fields

To configure fields for a page, you’re basically building an array of **`FieldConfig`** objects, one per UI control, using the **`FieldType`** enum to pick the widget and then layering on:

* label & translation
* required/optional
* when it shows (conditionals)
* how it’s validated
* extra config (options, address config, etc.)

I’ll use your **mother’s page** as a worked example and generalise as we go.

***

### 1. What a field actually is (`FieldConfig` + `FieldType`)

Each entry in the `fields` array is a [**`FieldConfig`**](https://github.com/opencrvs/opencrvs-core/blob/625cd2662a5101caac5a1f7b26a6c8ed77c27246/packages/commons/src/events/FieldConfig.ts) — the schema that the v2 engine uses to render and validate the control. The core type lives in `FieldConfig.ts`, and the allowed `type` values are defined by [`FieldType.ts`](https://fieldtype.tshttps/github.com/opencrvs/opencrvs-core/blob/625cd2662a5101caac5a1f7b26a6c8ed77c27246/packages/commons/src/events/FieldType.ts#L14) (e.g. `TEXT`, `CHECKBOX`, `NAME`, `AGE`, `ADDRESS`, `COUNTRY`, `SELECT`, `ID`, `DIVIDER`, `PARAGRAPH`, etc.)

At minimum you’ll always set:

* `id` – unique within the form; also becomes the path used in conditionals and validators (`field('mother.dob')`)
* `type` – one of `FieldType` (or `'DATE'` string in your example – effectively the same)
* `label` – a `TranslationConfig` for the UI text

Then you add optional behaviour:

* `required`, `hideLabel`, `analytics`, `secured`
* `conditionals` – show/hide and review behaviour
* `validation` – per-field and cross-field rules
* `configuration` – extra settings depending on field type
* `options` – for selects
* `defaultValue`

***

### 2. Step-by-step: designing the fields for a page

#### Step 1 – Decide the data + IDs

For each piece of data you want on the page, pick a **stable ID**.

Using the mother’s page for birth as an example:

* `mother.detailsNotAvailable`
* `mother.reason`
* `mother.name`
* `mother.dob`
* `mother.dobUnknown`
* `mother.age`
* `mother.nationality`
* `mother.idType`
* `mother.nid`, `mother.passport`, `mother.brn`
* `mother.address`, `mother.addressHelper`, `mother.addressDivider1`, `mother.addressDivider2`
* `mother.maritalStatus`, `mother.educationalAttainment`, `mother.occupation`, `mother.previousBirths`

This `mother.*` convention is what makes the conditions and validators readable (`field('mother.age')`, `field('mother.idType')`, etc.).

When you define a page for another role (father, informant, deceased, spouse), follow the same pattern: `father.*`, `informant.*`, etc.

***

#### Step 2 – Choose the `FieldType` for each input

Given the ID + domain meaning, pick a `FieldType`:

* **Booleans / toggles** → `FieldType.CHECKBOX`
  * e.g. `mother.detailsNotAvailable`, `mother.dobUnknown`
* **Free text** → `FieldType.TEXT`
  * e.g. `mother.reason`, `mother.occupation`
* **Names** → `FieldType.NAME` (composite widget with first/middle/last etc.)
  * `mother.name`
* **Dates** → `FieldType.DATE` (in your example it’s `'DATE'`, but in code you’d normally use the enum)
  * `mother.dob`
* **Numeric values** → `FieldType.AGE` or `FieldType.NUMBER`
  * `mother.age` (AGE), `mother.previousBirths` (NUMBER)
* **Select lists** → `FieldType.SELECT`
  * `mother.idType`, `mother.maritalStatus`, `mother.educationalAttainment`
* **Country** → `FieldType.COUNTRY`
  * `mother.nationality`
* **ID-like text** → `FieldType.ID` or `FieldType.TEXT` depending on validation needs
  * `mother.nid` (ID), `mother.passport` (TEXT), `mother.brn` (TEXT)
* **Address** → `FieldType.ADDRESS` (composite with nested validators)
  * `mother.address`
* **Layout / helpers (no data)** → `FieldType.DIVIDER`, `FieldType.PARAGRAPH`
  * `mother.details.divider`, `mother.addressDivider1`, `mother.addressDivider2`, `mother.addressHelper`

This list is not exhaustive, but a good example of common fields used in the **field palette** you get from `FieldType.ts` — you pick whichever widget makes sense for that piece of data.

{% hint style="info" %}
A full recipe of how to use each field isnt available at present, but we will build each recipe out in documentation over time, starting with the Address component.
{% endhint %}

***

#### Step 3 – Add labels (TranslationConfig)

Every field gets a `label`:

```ts
label: {
  defaultMessage: 'Age of mother',
  description: 'This is the label for the field',
  id: 'event.birth.action.declare.form.section.mother.field.age.label'
}
```

Patterns worth keeping:

* `defaultMessage` is human-readable English
* `description` helps translators
* `id` follows your event + section naming convention

For layout-only fields (DIVIDER) you reuse `emptyMessage`; for helper headings (PARAGRAPH) you give a more presentational label and optional `configuration.styles`.

***

#### Step 4 – Required / secured / analytics / hideLabel

You then decide how the field behaves e.g.:

* `required: true` – must be filled when it is mandatory or optional
  * e.g. `mother.name`, `mother.dob` or `mother.age`, `mother.idType`, `mother.address`
* `secured: true` – treated as sensitive PII (e.g. DOB, address)
  * e.g. `mother.dob`, `mother.address`
* `analytics: true` – flagged for metrics / reporting meaning the field will be accessible in Metabase
  * e.g. `mother.dob`, `mother.age`, `mother.maritalStatus`, `mother.educationalAttainment`, `mother.occupation`, `mother.previousBirths`
* `hideLabel: true` – when the control has an implicit label (e.g. NAME with inline placeholders) and you don’t want an extra label line
  * e.g. `mother.name`

This is where you encode **business importance** and **privacy** for each field.

***

#### Step 5 – Conditional visibility (`conditionals`)

This is the big one: **when** should each field show?

You use `conditionals: []` with:

* `type: ConditionalType.SHOW` – controls visibility in the main form
* `type: ConditionalType.DISPLAY_ON_REVIEW` – controls visibility in the review pane
* `conditional: ...` – expression built using helpers like `field()`, `user()`, `and()`, `or()`, `not()`, `never()`

Some patterns from the mother example:

**5.1. Entire “mother details not available” branch**

```ts
{
  id: 'mother.detailsNotAvailable',
  type: FieldType.CHECKBOX,
  conditionals: [
    {
      type: ConditionalType.SHOW,
      conditional: not(
        field('informant.relation').isEqualTo(InformantType.MOTHER)
      )
    },
    {
      type: ConditionalType.DISPLAY_ON_REVIEW,
      conditional: field('mother.detailsNotAvailable').isEqualTo(true)
    }
  ]
}
```

Key ideas:

* If the informant **is** the mother, you don’t even show the “details not available” checkbox (we assume they know their own details).
* On the review screen, only show this checkbox when it is **true**.

Same logic is reused for:

* `mother.details.divider` – only visible when informant is not the mother
* `mother.reason` – only visible if `detailsNotAvailable` is `true`

**5.2. “Mother details must be captured” branch**

Most fields use a `requireMotherDetails` helper:

```ts
conditionals: [
  {
    type: ConditionalType.SHOW,
    conditional: requireMotherDetails
  }
]
```

Where `requireMotherDetails` is (roughly) “mother details are required for this record”, factoring in:

* whether informant is mother, and
* whether the “details not available” checkbox is ticked

This keeps field configs readable and DRY.

**5.3. Mutually exclusive DOB / Age logic**

* `mother.dob` shows when:
  * `mother.dobUnknown` is **not** true, and
  * `requireMotherDetails` is true
* `mother.age` shows when:
  * `mother.dobUnknown` **is** true, and
  * `requireMotherDetails` is true

So users either give an exact date of birth **or** an age as of `child.dob`.

**5.4. ID type switching**

* `mother.nid` shows when `mother.idType === NATIONAL_ID`
* `mother.passport` shows when `mother.idType === PASSPORT`
* `mother.brn` shows when `mother.idType === BIRTH_REGISTRATION_NUMBER`

This is a classic pattern you’ll reuse elsewhere: pick an **ID type** via a SELECT, then show the relevant input field.

***

#### Step 6 – Validation rules (`validation`)

Each field can have multiple validation rules:

```ts
validation: [
  {
    message: { ... },
    validator: field('mother.dob').isBefore().now()
  },
  {
    message: { ... },
    validator: field('mother.dob').isBefore().date(field('child.dob'))
  }
]
```

Patterns in the mother page:

* **Simple date rule** – `mother.dob` must be before today.
* **Cross-field date rule** – `mother.dob` must be before `child.dob`.

Age field:

```ts
validation: [
  {
    validator: field('mother.age').asAge().isBetween(12, 120),
    message: { ... }
  }
]
```

National ID:

```ts
validation: [
  nationalIdValidator('mother.nid'),
  {
    message: { ... },
    validator: and(
      not(field('mother.nid').isEqualTo(field('father.nid'))),
      not(field('mother.nid').isEqualTo(field('informant.nid')))
    )
  }
]
```

Here you see:

* A reusable `nationalIdValidator`
* A cross-field uniqueness check vs father and informant

Address:

```ts
validation: [
  {
    message: { ... },
    validator: field('mother.address').isValidAdministrativeLeafLevel()
  },
  ...getNestedFieldValidators('mother.address', defaultStreetAddressConfiguration)
]
```

The address field uses:

* a top-level “leaf level” check (ensuring the lowest administrative level is selected)
* nested validators for street address sub-fields

When you build pages for other entities, you follow the same pattern: use core’s field helpers plus any custom validators to enforce your country’s rules.

***

#### Step 7 – Field-specific `configuration` and `options`

Some field types accept extra configuration:

* `NAME`:

  ```ts
  configuration: { maxLength: MAX_NAME_LENGTH }
  ```
* `AGE`:

  ```ts
  configuration: {
    asOfDate: field('child.dob'),
    postfix: { defaultMessage: 'years', ... }
  }
  ```
* `ADDRESS`:

{% hint style="warning" %}
The mandatory selects for administrative location hierarchy in an Address component are always required and configured in [4.2.5.1 Set up application settings](https://github.com/opencrvs/documentation/blob/master/v1.9.0/setup/3.-installation/3.2-set-up-your-own-country-configuration/3.2.5-set-up-application-settings), using the [4.2.2 administrative divisions](https://github.com/opencrvs/documentation/blob/master/v1.9.0/setup/3.-installation/3.2-set-up-your-own-country-configuration/3.2.2-set-up-administrative-address-divisions)
{% endhint %}

```ts
configuration: {
  streetAddressForm: defaultStreetAddressConfiguration
}
```

* `PARAGRAPH`:

  ```ts
  configuration: { styles: { fontVariant: 'h3' } }
  ```

For selects:

* `idTypeOptions`, `maritalStatusOptions`, `educationalAttainmentOptions`\
  are arrays of `{ value, label }` or similar, defined elsewhere with translation support.

This is where you plug in country-specific enumerations and UI presentation tweaks.

***

#### Step 8 – Defaults (`defaultValue`)

Finally, you can pre-fill fields:

* `mother.nationality` defaults to `'FAR'`:

  ```ts
  defaultValue: 'FAR'
  ```
* `mother.address` defaults to:

  * country: `'FAR'`
  * addressType: `DOMESTIC`
  * admin area: user’s district:

  ```ts
  defaultValue: {
    country: 'FAR',
    addressType: AddressType.DOMESTIC,
    administrativeArea: user('primaryOfficeId').locationLevel('district')
  }
  ```

This improves UX and ensures fields have sensible values for analytics.

***

### 3. Recipe for configuring fields on any page

For each page (mother, father, informant, deceased, etc.):

1. **List the information you need** for that actor.
2. **Choose a stable ID** for each piece (`<actor>.<property>`).
3. **Pick a `FieldType`** from `FieldType.ts` that best matches the UI shape.
4. **Add a `label`** with proper `defaultMessage` and translation `id`.
5. Set **behaviour flags**:
   * `required`, `secured`, `analytics`, `hideLabel` where appropriate.
6. Configure **conditionals**:
   * `SHOW` + logical helpers (`field()`, `and`, `or`, `not`, `never`)
   * optionally `DISPLAY_ON_REVIEW` if review view should differ
7. Add **validation rules**:
   * simple type rules (date, number, length)
   * cross-field rules (mother vs child, uniqueness constraints)
   * nested rules for ADDRESS, etc.
8. Add **`configuration` and `options`** for complex widgets:
   * NAME, AGE, ADDRESS, SELECT, PARAGRAPH styling, etc.
9. Set **`defaultValue`** where it makes sense (country, address, etc.).

That’s essentially how you configure form fields for each page.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://documentation.opencrvs.org/v1.9/setup/3.-installation/3.2-set-up-your-own-country-configuration/3.2.7-configure-declaration-forms/4.2.6.2-configure-an-event-declaration-form.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
