# Forms & Surveys

Form definitions, submissions, lead capture, and in-conversation surveys


Capture leads with embeddable forms and run channel-scoped surveys inside Messenger conversations, including one-vote-per-user enforcement and survey-driven ticket automation.

Backend modules: `backend/plugins/frontline_api/src/modules/{form,survey}`. Frontend: `frontend/plugins/frontline_ui/src/modules/{forms,survey}` at `/frontline/forms` and `/frontline/surveys`.

## Forms

### Definitions and fields

A `Form` carries `name`, `title`, `code`, `type`, `description`, `buttonText`, `numberOfPages`, `visibility`, `leadData`, `languageCode`, `departmentIds`, `tagIds`, `channelId`, and `integrationId`. Its fields are `FrontlineField` records managed through the `frontlineFields*` operations (each with `validation`, `regexValidation`, `validator`, `options`, `order`, `isRequired`, `logics`, and `subFields`). Field validation is defined by a `FieldValidator` (`type` of `PRESET`/`CUSTOM`/`NONE`, `presetKey`, `customRegex`, `errorMessage`) backed by [`form/validationRegistry.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/frontline_api/src/modules/form/validationRegistry.ts). Preset keys: `EMAIL`, `NUMBER`, `DATE_TIME`, `DATE`, `PHONE`, `MN_VEHICLE_REGISTRATION`, `POSTAL_CODE`, `ALPHANUMERIC`. Custom regex support is guarded by length/time limits to avoid pathological patterns.

### Submissions

A `Submission` groups one customer's answers (`contentTypeId`, `formId`, `conversationId`, `channelId`, `customerId`, `customFieldsData`, `submissions`); each `FormSubmission` row stores `formFieldId`, the submitted `text`/`value`, `formFieldText`/`formFieldType`, and `submittedAt`. Submissions attach to the customer record and can be logged onto related entities through `relation.onRelationAdded`. Submission export is registered as content type `frontline:formSubmission.formSubmission` (permission `formSubmissionsExportManage`).

### Form queries and mutations

Schema: [`modules/form/graphql/schema/form.ts`](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/frontline_api/src/modules/form/graphql/schema/form.ts).

| Operation | Kind | Purpose |
| --- | --- | --- |
| `forms` / `formsMain` / `formsTotalCount` / `formDetail(_id)` | query | Form list/detail with `type`, `channelId`, `tagId`, `status`, `searchValue` filters |
| `formSubmissions(filter…)` / `formSubmissionDetail(_id)` / `formsGetContentTypes` | query | Submission reads |
| `cpForms` / `cpFormDetail` | query | Client portal reads |
| `formsAdd` / `formsEdit` / `formsRemove` / `formsToggleStatus` / `formsDuplicate` | mutation | Form management |
| `frontlineFields` / `frontlineFieldByCode` / `frontlineFieldsGetTypes` / `frontlineFieldsCombinedByContentType` | query | Field reads |
| `frontlineFieldsAdd` / `frontlineFieldsEdit` / `frontlineFieldsRemove` / `frontlineFieldsBulkAction` / `frontlineFieldsUpdateOrder` / `frontlineFieldsUpdateVisible` / `frontlineFieldsUpdateSystemFields` | mutation | Field management |
| `formSubmissionsSave` / `formSubmissionsRemove` / `formSubmissionsEdit` | mutation | Submission writes |
| `widgetsLeadConnect` / `widgetsSaveLead` | mutation | Widget lead flow: connect a lead integration, then save a submission |
| `cpWidgetsSaveLead` | mutation | Client portal lead submission |

```graphql
mutation {
  formsAdd(
    name: "Contact us"
    title: "Contact us"
    type: "lead"
    channelId: "chan_1"
  ) {
    _id
    code
  }
}
```

### tRPC

`form.submissionsByConversation` returns a conversation's form submissions.

## Surveys

Survey operations are named `survey*` (older code called them polls: `poll*`, `withPoll`, `Ticket.sourcePoll`).

### Definitions

A `Survey` carries `title`, `question`, `channelId`, `brandId`, `code`, `options`, ordered `steps` (each with its own `question` and `options`), `allowMultiselect`, `durationHours`, `status` (`active` or `archived`), `sentCount`, and `createdUserId`. Options can arm ticket automation (`ticketCreationEnabled`, `ticketCreationThreshold`, `ticketPipelineId`, `ticketStatusId`, `ticketCreated`, `ticketId`).

### Sending into a conversation

`surveySendToConversation(_id, conversationId)` is a mutation that:

- Requires an `active` survey.
- Only targets Messenger conversations.
- Checks channel and brand compatibility between the survey and conversation.
- Writes a survey snapshot onto the message's `extraData.survey` and marks the conversation `hasSurvey: true`, which feeds the `withSurvey` inbox filter.

> [!WARNING]
> Survey conversations are excluded from `integrationType`-scoped inbox lists unless you pass `withSurvey: "true"`.

### Client portal submission

`cpSurveySubmit(surveyCode, optionIds)` requires an authenticated client portal user. It finds or creates the customer, creates a Messenger conversation with a survey snapshot, validates option ids and `allowMultiselect` rules, and returns `alreadyVoted` on repeat votes. A partial unique index on `SurveyVotes` (`{ surveyId: 1, cpUserId: 1 }`, filtered to documents where `cpUserId` exists) enforces one vote per client-portal user per survey ([definitions](https://github.com/erxes/erxes/blob/f91e8c6f4714c910a32c6b6e14fe085f6435c89e/backend/plugins/frontline_api/src/modules/survey/db/definitions/surveys.ts)). After a successful vote, `runSurveyTicketAutomation` evaluates armed options; see [Tickets](https://erxes.io/docs/frontline-tickets#survey-driven-ticket-automation).

### Survey queries and mutations

| Operation | Kind | Purpose |
| --- | --- | --- |
| `surveyList` / `surveyDetail(_id)` / `surveyTotalCount` | query | Admin list/detail with `searchValue`, `status`, `channelId` filters |
| `cpSurveys` / `cpSurveyDetail(channelId, surveyCode)` / `cpSurveyVotes(conversationId)` | query | Client portal list, detail, and existing vote selections |
| `surveyAdd` / `surveyEdit` / `surveyRemove` / `surveyToggleStatus` | mutation | Survey management |
| `surveySendToConversation(_id, conversationId)` | mutation | Inject the survey into a Messenger conversation |
| `cpSurveySubmit(surveyCode, optionIds)` | mutation | Client portal vote |

There is no `cpSurveyVote` operation; the read is `cpSurveyVotes` and the write is `cpSurveySubmit`.

