diff --git a/16/umbraco-forms/.gitbook.yaml b/16/umbraco-forms/.gitbook.yaml
new file mode 100644
index 00000000000..b047897620f
--- /dev/null
+++ b/16/umbraco-forms/.gitbook.yaml
@@ -0,0 +1,8 @@
+root: ./
+
+​structure:
+ readme: README.md
+ summary: SUMMARY.md
+
+redirects:
+
diff --git a/16/umbraco-forms/.gitbook/assets/Blog_post_for_com_900x400px_1_8_7_ (1).png b/16/umbraco-forms/.gitbook/assets/Blog_post_for_com_900x400px_1_8_7_ (1).png
new file mode 100644
index 00000000000..2a8c064f60e
Binary files /dev/null and b/16/umbraco-forms/.gitbook/assets/Blog_post_for_com_900x400px_1_8_7_ (1).png differ
diff --git a/16/umbraco-forms/.gitbook/assets/Blog_post_for_com_900x400px_1_8_7_.png b/16/umbraco-forms/.gitbook/assets/Blog_post_for_com_900x400px_1_8_7_.png
new file mode 100644
index 00000000000..2a8c064f60e
Binary files /dev/null and b/16/umbraco-forms/.gitbook/assets/Blog_post_for_com_900x400px_1_8_7_.png differ
diff --git a/16/umbraco-forms/.gitbook/assets/Documentations Icons_Umbraco_Forms_Creating_A_Form.png b/16/umbraco-forms/.gitbook/assets/Documentations Icons_Umbraco_Forms_Creating_A_Form.png
new file mode 100644
index 00000000000..f900855ff15
Binary files /dev/null and b/16/umbraco-forms/.gitbook/assets/Documentations Icons_Umbraco_Forms_Creating_A_Form.png differ
diff --git a/16/umbraco-forms/.gitbook/assets/Documentations Icons_Umbraco_Forms_Frontend.png b/16/umbraco-forms/.gitbook/assets/Documentations Icons_Umbraco_Forms_Frontend.png
new file mode 100644
index 00000000000..433b9c526ec
Binary files /dev/null and b/16/umbraco-forms/.gitbook/assets/Documentations Icons_Umbraco_Forms_Frontend.png differ
diff --git a/16/umbraco-forms/.gitbook/assets/Documentations Icons_Umbraco_Forms_Install.png b/16/umbraco-forms/.gitbook/assets/Documentations Icons_Umbraco_Forms_Install.png
new file mode 100644
index 00000000000..e4aa739cb56
Binary files /dev/null and b/16/umbraco-forms/.gitbook/assets/Documentations Icons_Umbraco_Forms_Install.png differ
diff --git a/16/umbraco-forms/.gitbook/assets/FormSettingsAutocomplete (1).png b/16/umbraco-forms/.gitbook/assets/FormSettingsAutocomplete (1).png
new file mode 100644
index 00000000000..148c59918e3
Binary files /dev/null and b/16/umbraco-forms/.gitbook/assets/FormSettingsAutocomplete (1).png differ
diff --git a/16/umbraco-forms/.gitbook/assets/FormSettingsAutocomplete.png b/16/umbraco-forms/.gitbook/assets/FormSettingsAutocomplete.png
new file mode 100644
index 00000000000..148c59918e3
Binary files /dev/null and b/16/umbraco-forms/.gitbook/assets/FormSettingsAutocomplete.png differ
diff --git a/16/umbraco-forms/.gitbook/assets/FormSettingsFieldsDisplayed.png b/16/umbraco-forms/.gitbook/assets/FormSettingsFieldsDisplayed.png
new file mode 100644
index 00000000000..18cc4170619
Binary files /dev/null and b/16/umbraco-forms/.gitbook/assets/FormSettingsFieldsDisplayed.png differ
diff --git a/16/umbraco-forms/.gitbook/assets/Umbraco 9.3RC- Blog_post_for_com_900x400px_1@2x-80 (1).jpg b/16/umbraco-forms/.gitbook/assets/Umbraco 9.3RC- Blog_post_for_com_900x400px_1@2x-80 (1).jpg
new file mode 100644
index 00000000000..d12b2963fc0
Binary files /dev/null and b/16/umbraco-forms/.gitbook/assets/Umbraco 9.3RC- Blog_post_for_com_900x400px_1@2x-80 (1).jpg differ
diff --git a/16/umbraco-forms/.gitbook/assets/Umbraco 9.3RC- Blog_post_for_com_900x400px_1@2x-80.jpg b/16/umbraco-forms/.gitbook/assets/Umbraco 9.3RC- Blog_post_for_com_900x400px_1@2x-80.jpg
new file mode 100644
index 00000000000..d12b2963fc0
Binary files /dev/null and b/16/umbraco-forms/.gitbook/assets/Umbraco 9.3RC- Blog_post_for_com_900x400px_1@2x-80.jpg differ
diff --git a/16/umbraco-forms/.gitbook/assets/_packageBlog_post_for_com_900x400px (1).png b/16/umbraco-forms/.gitbook/assets/_packageBlog_post_for_com_900x400px (1).png
new file mode 100644
index 00000000000..824e289115a
Binary files /dev/null and b/16/umbraco-forms/.gitbook/assets/_packageBlog_post_for_com_900x400px (1).png differ
diff --git a/16/umbraco-forms/.gitbook/assets/_packageBlog_post_for_com_900x400px.png b/16/umbraco-forms/.gitbook/assets/_packageBlog_post_for_com_900x400px.png
new file mode 100644
index 00000000000..824e289115a
Binary files /dev/null and b/16/umbraco-forms/.gitbook/assets/_packageBlog_post_for_com_900x400px.png differ
diff --git a/16/umbraco-forms/.gitbook/assets/image.png b/16/umbraco-forms/.gitbook/assets/image.png
new file mode 100644
index 00000000000..002dbcbe5c8
Binary files /dev/null and b/16/umbraco-forms/.gitbook/assets/image.png differ
diff --git a/16/umbraco-forms/.gitbook/assets/umbraco_forms_swagger.json b/16/umbraco-forms/.gitbook/assets/umbraco_forms_swagger.json
new file mode 100644
index 00000000000..360a0c02bf4
--- /dev/null
+++ b/16/umbraco-forms/.gitbook/assets/umbraco_forms_swagger.json
@@ -0,0 +1,555 @@
+{
+ "openapi": "3.0.1",
+ "info": {
+ "title": "Umbraco Forms API",
+ "description": "Describes the Umbraco Forms API available for rendering and submitting forms. You can find out more about the API in [the documentation](https://docs.umbraco.com/umbraco-forms/v/12.forms.latest/developer/ajaxforms)",
+ "version": "Latest"
+ },
+ "paths": {
+ "/umbraco/forms/delivery/api/v1/definitions/{id}": {
+ "get": {
+ "tags": [
+ "Forms"
+ ],
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "description": "The form's Id.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "format": "uuid"
+ }
+ },
+ {
+ "name": "contentId",
+ "in": "query",
+ "description": "The Id of the content page on which the form is hosted.",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "culture",
+ "in": "query",
+ "description": "The culture code for the form's localization context.",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "additionalData",
+ "in": "query",
+ "description": "Additional data provided when rendering the form.",
+ "schema": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/FormDto"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not Found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/umbraco/forms/delivery/api/v1/entries/{id}": {
+ "post": {
+ "tags": [
+ "Forms"
+ ],
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "description": "The form's Id.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "format": "uuid"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/FormEntryDto"
+ }
+ }
+ }
+ },
+ "responses": {
+ "202": {
+ "description": "Accepted"
+ },
+ "400": {
+ "description": "Bad Request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not Found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Client Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetails"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "FieldConditionActionType": {
+ "enum": [
+ "Show",
+ "Hide"
+ ],
+ "type": "integer",
+ "format": "int32"
+ },
+ "FieldConditionLogicType": {
+ "enum": [
+ "All",
+ "Any"
+ ],
+ "type": "integer",
+ "format": "int32"
+ },
+ "FieldConditionRuleOperator": {
+ "enum": [
+ "Is",
+ "IsNot",
+ "GreaterThen",
+ "LessThen",
+ "Contains",
+ "StartsWith",
+ "EndsWith"
+ ],
+ "type": "integer",
+ "format": "int32"
+ },
+ "FormConditionDto": {
+ "type": "object",
+ "properties": {
+ "actionType": {
+ "$ref": "#/components/schemas/FieldConditionActionType"
+ },
+ "logicType": {
+ "$ref": "#/components/schemas/FieldConditionLogicType"
+ },
+ "rules": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/FormConditionRuleDto"
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ "FormConditionRuleDto": {
+ "type": "object",
+ "properties": {
+ "field": {
+ "type": "string"
+ },
+ "operator": {
+ "$ref": "#/components/schemas/FieldConditionRuleOperator"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "FormDto": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "format": "uuid"
+ },
+ "name": {
+ "type": "string"
+ },
+ "indicator": {
+ "type": "string"
+ },
+ "cssClass": {
+ "type": "string",
+ "nullable": true
+ },
+ "nextLabel": {
+ "type": "string",
+ "nullable": true
+ },
+ "previousLabel": {
+ "type": "string",
+ "nullable": true
+ },
+ "submitLabel": {
+ "type": "string",
+ "nullable": true
+ },
+ "disableDefaultStylesheet": {
+ "type": "boolean"
+ },
+ "fieldIndicationType": {
+ "$ref": "#/components/schemas/FormFieldIndication"
+ },
+ "hideFieldValidation": {
+ "type": "boolean"
+ },
+ "messageOnSubmit": {
+ "type": "string",
+ "nullable": true
+ },
+ "messageOnSubmitIsHtml": {
+ "type": "boolean"
+ },
+ "showValidationSummary": {
+ "type": "boolean"
+ },
+ "gotoPageOnSubmit": {
+ "type": "string",
+ "format": "uuid",
+ "nullable": true
+ },
+ "gotoPageOnSubmitRoute": {
+ "$ref": "#/components/schemas/IApiContentRouteModel"
+ },
+ "pages": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/FormPageDto"
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ "FormEntryDto": {
+ "type": "object",
+ "properties": {
+ "values": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "contentId": {
+ "type": "string",
+ "nullable": true
+ },
+ "culture": {
+ "type": "string",
+ "nullable": true
+ },
+ "additionalData": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ },
+ "nullable": true
+ }
+ },
+ "additionalProperties": false
+ },
+ "FormFieldDto": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "format": "uuid"
+ },
+ "caption": {
+ "type": "string"
+ },
+ "helpText": {
+ "type": "string",
+ "nullable": true
+ },
+ "placeholder": {
+ "type": "string",
+ "nullable": true
+ },
+ "cssClass": {
+ "type": "string",
+ "nullable": true
+ },
+ "alias": {
+ "type": "string"
+ },
+ "required": {
+ "type": "boolean"
+ },
+ "requiredErrorMessage": {
+ "type": "string",
+ "nullable": true
+ },
+ "pattern": {
+ "type": "string",
+ "nullable": true
+ },
+ "patternInvalidErrorMessage": {
+ "type": "string",
+ "nullable": true
+ },
+ "condition": {
+ "$ref": "#/components/schemas/FormConditionDto"
+ },
+ "fileUploadOptions": {
+ "$ref": "#/components/schemas/FormFileUploadOptionsDto"
+ },
+ "preValues": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/FormFieldPrevalueDto"
+ }
+ },
+ "settings": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "type": {
+ "$ref": "#/components/schemas/FormFieldTypeDto"
+ }
+ },
+ "additionalProperties": false
+ },
+ "FormFieldIndication": {
+ "enum": [
+ "NoIndicator",
+ "MarkMandatoryFields",
+ "MarkOptionalFields"
+ ],
+ "type": "integer",
+ "format": "int32"
+ },
+ "FormFieldPrevalueDto": {
+ "type": "object",
+ "properties": {
+ "value": {
+ "type": "string"
+ },
+ "caption": {
+ "type": "string",
+ "nullable": true
+ }
+ },
+ "additionalProperties": false
+ },
+ "FormFieldTypeDto": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "format": "uuid"
+ },
+ "name": {
+ "type": "string"
+ },
+ "supportsPreValues": {
+ "type": "boolean"
+ },
+ "supportsUploadTypes": {
+ "type": "boolean"
+ },
+ "renderInputType": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "FormFieldsetColumnDto": {
+ "type": "object",
+ "properties": {
+ "caption": {
+ "type": "string",
+ "nullable": true
+ },
+ "width": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "fields": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/FormFieldDto"
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ "FormFieldsetDto": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "format": "uuid"
+ },
+ "caption": {
+ "type": "string",
+ "nullable": true
+ },
+ "condition": {
+ "$ref": "#/components/schemas/FormConditionDto"
+ },
+ "columns": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/FormFieldsetColumnDto"
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ "FormFileUploadOptionsDto": {
+ "type": "object",
+ "properties": {
+ "allowAllUploadExtensions": {
+ "type": "boolean"
+ },
+ "allowedUploadExtensions": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "allowMultipleFileUploads": {
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": false
+ },
+ "FormPageDto": {
+ "type": "object",
+ "properties": {
+ "caption": {
+ "type": "string",
+ "nullable": true
+ },
+ "condition": {
+ "$ref": "#/components/schemas/FormConditionDto"
+ },
+ "fieldsets": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/FormFieldsetDto"
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ "IApiContentRouteModel": {
+ "type": "object",
+ "properties": {
+ "path": {
+ "type": "string",
+ "readOnly": true
+ },
+ "startItem": {
+ "$ref": "#/components/schemas/IApiContentStartItemModel"
+ }
+ },
+ "additionalProperties": false
+ },
+ "IApiContentStartItemModel": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "format": "uuid",
+ "readOnly": true
+ },
+ "path": {
+ "type": "string",
+ "readOnly": true
+ }
+ },
+ "additionalProperties": false
+ },
+ "ProblemDetails": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "nullable": true
+ },
+ "title": {
+ "type": "string",
+ "nullable": true
+ },
+ "status": {
+ "type": "integer",
+ "format": "int32",
+ "nullable": true
+ },
+ "detail": {
+ "type": "string",
+ "nullable": true
+ },
+ "instance": {
+ "type": "string",
+ "nullable": true
+ }
+ },
+ "additionalProperties": { }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/16/umbraco-forms/README.md b/16/umbraco-forms/README.md
new file mode 100644
index 00000000000..c0cc5146f23
--- /dev/null
+++ b/16/umbraco-forms/README.md
@@ -0,0 +1,27 @@
+---
+description: >-
+ Documentation on how to work with Umbraco Forms for both editors and
+ developers.
+---
+
+# Umbraco Forms Documentation
+
+Umbraco Forms is a tool that lets you build forms of all shapes and sizes and put them on your Umbraco websites. Build forms using a long list of elements like multiple choice, dropdowns, text areas and checkboxes. Choose between a series of different workflows and control what happens once a form has been submitted.
+
+[Purchase Umbraco Forms](https://umbraco.com/products/umbraco-forms/) or sign up for an [Umbraco Cloud](https://try.umbraco.com/) project where Umbraco Forms is part of the package.
+
+
+
+## Quick Links
+
+{% content-ref url="upgrading/manualupgrade.md" %}
+[manualupgrade.md](upgrading/manualupgrade.md)
+{% endcontent-ref %}
+
+{% content-ref url="editor/attaching-workflows/" %}
+[attaching-workflows](editor/attaching-workflows/)
+{% endcontent-ref %}
+
+{% content-ref url="developer/forms-in-the-database.md" %}
+[forms-in-the-database.md](developer/forms-in-the-database.md)
+{% endcontent-ref %}
diff --git a/16/umbraco-forms/SUMMARY.md b/16/umbraco-forms/SUMMARY.md
new file mode 100644
index 00000000000..4af4cd066de
--- /dev/null
+++ b/16/umbraco-forms/SUMMARY.md
@@ -0,0 +1,77 @@
+# Table of contents
+
+* [Umbraco Forms Documentation](README.md)
+* [Legacy Documentation](legacy-documentation.md)
+* [Release Notes](release-notes.md)
+
+## Installation
+
+* [Installing Umbraco Forms](installation/install.md)
+* [Licensing](installation/the-licensing-model.md)
+
+## Upgrading
+
+* [Upgrading Umbraco Forms](upgrading/manualupgrade.md)
+* [Version Specific Upgrade Notes](upgrading/version-specific.md)
+* [Migration IDs](upgrading/migration-ids.md)
+
+## Editor
+
+* [Creating a Form - The basics](editor/creating-a-form/README.md)
+ * [Form Settings](editor/creating-a-form/form-settings.md)
+ * [Form Advanced Options](editor/creating-a-form/form-advanced.md)
+ * [Form Information](editor/creating-a-form/form-info.md)
+ * [Overview Of The Field Types](editor/creating-a-form/fieldtypes/README.md)
+ * [Date](editor/creating-a-form/fieldtypes/date.md)
+ * [File Upload](editor/creating-a-form/fieldtypes/fileupload.md)
+ * [reCAPTCHA V2](editor/creating-a-form/fieldtypes/recaptcha2.md)
+ * [reCAPTCHA V3](editor/creating-a-form/fieldtypes/recaptcha3.md)
+ * [Setting-up Conditional Logic on Fields](editor/creating-a-form/conditional-logic.md)
+* [Attaching Workflows](editor/attaching-workflows/README.md)
+ * [Workflow Types](editor/attaching-workflows/workflow-types.md)
+* [Viewing And Exporting Entries](editor/viewing-and-exporting-entries.md)
+* [Defining And Attaching Prevalue Sources](editor/defining-and-attaching-prevaluesources/README.md)
+ * [Prevalue Source Types Overview](editor/defining-and-attaching-prevaluesources/prevalue-source-types.md)
+
+## Developer
+
+* [Property Editors](developer/property-editors.md)
+* [Preparing Your Frontend](developer/prepping-frontend.md)
+* [Rendering Forms](developer/rendering-forms.md)
+* [Rendering Forms Scripts](developer/rendering-scripts.md)
+* [Themes](developer/themes.md)
+* [Custom Markup](developer/custom-markup.md)
+* [Email Templates](developer/email-templates.md)
+* [Working With Record Data](developer/working-with-data.md)
+* [Umbraco Forms in the Database](developer/forms-in-the-database.md)
+* [Extending](developer/extending/README.md)
+ * [Adding A Type To The Provider Model](developer/extending/adding-a-type.md)
+ * [Setting Types](developer/extending/setting-types.md)
+ * [Adding A Field Type To Umbraco Forms](developer/extending/adding-a-fieldtype.md)
+ * [Excluding a built-in field](developer/extending/excluding-a-built-in-field.md)
+ * [Adding A Prevalue Source Type To Umbraco Forms](developer/extending/adding-a-prevaluesourcetype.md)
+ * [Adding A Workflow Type To Umbraco Forms](developer/extending/adding-a-workflowtype.md)
+ * [Adding An Export Type To Umbraco Forms](developer/extending/adding-a-exporttype.md)
+ * [Adding a Magic String Format Function](developer/extending/adding-a-magic-string-format-function.md)
+ * [Adding A Server-Side Notification Handler To Umbraco Forms](developer/extending/adding-an-event-handler.md)
+ * [Adding a Validation Pattern](developer/extending/adding-a-validation-pattern.md)
+ * [Customize Default Fields and Workflows For a Form](developer/extending/customize-default-workflows.md)
+* [Configuration](developer/configuration/README.md)
+ * [Forms Provider Type Details](developer/configuration/type-details.md)
+* [Webhooks](developer/webhooks.md)
+* [Security](developer/security.md)
+* [Magic Strings](developer/magic-strings.md)
+* [Health Checks](developer/healthchecks/README.md)
+ * [Apply keys and indexes](developer/healthchecks/apply-keys.md)
+ * [Apply keys and indexes for forms in the database](developer/healthchecks/forms-in-the-database-apply-keys.md)
+* [Localization](developer/localization.md)
+* [Headless/AJAX Forms](developer/ajaxforms.md)
+* [Block List Labels](developer/blocklistlabels.md)
+* [Field Types](developer/field-types.md)
+* [Storing Prevalue Text Files With IPreValueTextFileStorage](developer/iprevaluetextfilestorage.md)
+
+## Tutorials
+
+* [Overview](tutorials/overview.md)
+* [Creating a Contact Form](tutorials/creating-a-contact-form.md)
+* [Creating a Multi-Page Form](tutorials/creating-a-multipage-form.md)
diff --git a/16/umbraco-forms/developer/ajaxforms.md b/16/umbraco-forms/developer/ajaxforms.md
new file mode 100644
index 00000000000..ba1e678dab6
--- /dev/null
+++ b/16/umbraco-forms/developer/ajaxforms.md
@@ -0,0 +1,600 @@
+# Headless/AJAX Forms
+
+Umbraco Forms provides an API for client-side rendering and submission of forms. This will be useful when you want to handle forms in a headless style scenario.
+
+## Enabling the API
+
+The Forms API is disabled by default. To enable it, set the `Umbraco:Forms:Options:EnableFormsApi` configuration key to `true`.
+
+For example:
+
+```json
+ "Umbraco": {
+ "Forms": {
+ "Options": {
+ "EnableFormsApi": true
+ }
+ }
+ }
+```
+
+## API Definition
+
+The API supports two endpoints, one for rendering a form and one for submitting it.
+
+{% swagger src="../.gitbook/assets/umbraco_forms_swagger.json" path="/umbraco/forms/delivery/api/v1/definitions/{id}" method="get" %}
+[umbraco_forms_swagger.json](../.gitbook/assets/umbraco_forms_swagger.json)
+{% endswagger %}
+
+{% swagger src="../.gitbook/assets/umbraco_forms_swagger.json" path="/umbraco/forms/delivery/api/v1/entries/{id}" method="post" %}
+[umbraco_forms_swagger.json](../.gitbook/assets/umbraco_forms_swagger.json)
+{% endswagger %}
+
+As well as this documentation, the definition of the API can also be reviewed via the Swagger UI.
+
+This is available alongside the Umbraco 12 Content Delivery Api at: `/umbraco/swagger/index.html`. Select "Umbraco Forms API" from the "Select a definition" list to view the methods available.
+
+
+
+The Open API specification is available from: `/umbraco/swagger/forms/swagger.json`
+
+### Requesting a Form Definition
+
+To request the definition of a form, the following request can be made:
+
+```none
+GET /umbraco/forms/delivery/api/v1/definitions/{id}?contentId={contentId}&culture={culture}&additionalData[{key}]={value}&additionalData[key2]={value2}
+```
+
+The GET request requires the Guid identifying the form.
+
+An optional `contentId` parameter can be provided, which can either be the integer or GUID identifier for the current page. If provided, the content item identified will be used for Forms features requiring information from the page the form is hosted on. This includes the parsing of ["magic string" placeholders](magic-strings.md).
+
+A `culture` parameter can also be provided, expected as an ISO code identifying a language used in the Umbraco installation (for example, `en-US`). This will be used to ensure the correct translation for dictionary keys is used. It will also retrieve page content from the appropriate language variant. If the parameter is not provided in the request, the default Umbraco language will be used.
+
+Finally, an `additionalData` parameter can be provided as a dictionary. This information will be made available when rendering the form allowing it to be used as a source for ["magic string" replacements](./magic-strings.md).
+
+If the requested form is not found, a 404 status code will be returned.
+
+A successful request will return a 200 status code. An example response is as follows. It will differ depending on the pages, fields and other settings available for the form.
+
+```json
+{
+ "disableDefaultStylesheet": false,
+ "fieldIndicationType": "MarkMandatoryFields",
+ "hideFieldValidation": false,
+ "id": "34ef4a19-efa7-40c1-b8b6-2fd7257f2ed3",
+ "indicator": "*",
+ "messageOnSubmit": "Thanks for submitting the form",
+ "name": "Simple Comment Form",
+ "nextLabel": "Next",
+ "pages": [
+ {
+ "caption": "Your comment",
+ "fieldsets": [
+ {
+ "caption": "",
+ "columns": [
+ {
+ "caption": "",
+ "width": 12,
+ "fields": [
+ {
+ "alias": "name",
+ "caption": "Name",
+ "condition": {
+ "actionType": "Show",
+ "logicType": "All",
+ "rules": []
+ },
+ "helpText": "[#message] from [#pageName]",
+ "id": "25185934-9a61-491c-9610-83dfe774662c",
+ "pattern": "",
+ "patternInvalidErrorMessage": "Please provide a valid value for Name",
+ "placeholder": "",
+ "preValues": [],
+ "required": true,
+ "requiredErrorMessage": "Please provide a value for Name",
+ "settings": {
+ "defaultValue": "",
+ "placeholder": "Please enter your name.",
+ "showLabel": "",
+ "maximumLength": "",
+ "fieldType": "",
+ "autocompleteAttribute": ""
+ },
+ "type": {
+ "id": "3f92e01b-29e2-4a30-bf33-9df5580ed52c",
+ "name": "Short answer"
+ }
+ },
+ {
+ "alias": "email",
+ "caption": "Email",
+ "condition": {
+ "actionType": "Show",
+ "logicType": "All",
+ "rules": []
+ },
+ "helpText": "",
+ "id": "816fdf3b-a796-4677-a317-943a54bf9d55",
+ "pattern": "^[_a-z0-9-]+(\\.[_a-z0-9-]+)*@[a-z0-9-]+(\\.[a-z0-9-]+)*(\\.[a-z]{2,4})$",
+ "patternInvalidErrorMessage": "Please provide a valid value for Email",
+ "placeholder": "",
+ "preValues": [],
+ "required": true,
+ "requiredErrorMessage": "Please provide a value for Email",
+ "settings": {
+ "defaultValue": "",
+ "placeholder": "",
+ "showLabel": "",
+ "maximumLength": "",
+ "fieldType": "email",
+ "autocompleteAttribute": ""
+ },
+ "type": {
+ "id": "3f92e01b-29e2-4a30-bf33-9df5580ed52c",
+ "name": "Short answer"
+ }
+ },
+ {
+ "alias": "comment",
+ "caption": "Comment",
+ "condition": {
+ "actionType": "Show",
+ "logicType": "All",
+ "rules": []
+ },
+ "helpText": "",
+ "id": "9d723100-ec34-412f-aaa5-516634d7c833",
+ "pattern": "",
+ "patternInvalidErrorMessage": "Please provide a valid value for Comment",
+ "placeholder": "",
+ "preValues": [],
+ "required": false,
+ "requiredErrorMessage": "Please provide a value for Comment",
+ "settings": {
+ "defaultValue": "",
+ "placeholder": "",
+ "showLabel": "",
+ "autocompleteAttribute": "",
+ "numberOfRows": "2",
+ "maximumLength": ""
+ },
+ "type": {
+ "id": "023f09ac-1445-4bcb-b8fa-ab49f33bd046",
+ "name": "Long answer"
+ }
+ },
+ {
+ "alias": "country",
+ "caption": "Country",
+ "condition": {
+ "actionType": "Show",
+ "logicType": "All",
+ "rules": []
+ },
+ "helpText": "",
+ "id": "30ff8f37-28d4-47df-f281-422b36c62e73",
+ "pattern": "",
+ "patternInvalidErrorMessage": "Please provide a valid value for Country",
+ "placeholder": "",
+ "preValues": [
+ {
+ "caption": "France",
+ "value": "fr"
+ },
+ {
+ "caption": "Italy",
+ "value": "it"
+ },
+ {
+ "caption": "Span",
+ "value": "es"
+ },
+ {
+ "caption": "United Kingdom",
+ "value": "gb"
+ }
+ ],
+ "required": false,
+ "requiredErrorMessage": "Please provide a value for Country",
+ "settings": {
+ "defaultValue": "",
+ "allowMultipleSelections": "",
+ "showLabel": "",
+ "autocompleteAttribute": "",
+ "selectPrompt": "Please select"
+ },
+ "type": {
+ "id": "0dd29d42-a6a5-11de-a2f2-222256d89593",
+ "name": "Dropdown"
+ }
+ },
+ {
+ "alias": "favouriteColour",
+ "caption": "Favourite Colour",
+ "condition": {
+ "actionType": "Show",
+ "logicType": "All",
+ "rules": []
+ },
+ "helpText": "",
+ "id": "a6e2e27f-097d-476a-edb9-4aa79449ab5c",
+ "pattern": "",
+ "patternInvalidErrorMessage": "Please provide a valid value for Favourite Colour",
+ "placeholder": "",
+ "preValues": [
+ {
+ "caption": "Red",
+ "value": "red"
+ },
+ {
+ "caption": "Green",
+ "value": "green"
+ },
+ {
+ "caption": "Yellow",
+ "value": "yello"
+ }
+ ],
+ "required": false,
+ "requiredErrorMessage": "Please provide a value for Favourite Colour",
+ "settings": {
+ "defaultValue": "",
+ "showLabel": ""
+ },
+ "type": {
+ "id": "fab43f20-a6bf-11de-a28f-9b5755d89593",
+ "name": "Multiple choice"
+ }
+ },
+ {
+ "alias": "dataConsent",
+ "caption": "Data consent",
+ "condition": {
+ "actionType": "Show",
+ "logicType": "All",
+ "rules": []
+ },
+ "helpText": "Please indicate if it's OK to store your data.",
+ "id": "9f25acaf-4ac4-4105-9afe-eb0bb0c03b31",
+ "pattern": "",
+ "patternInvalidErrorMessage": "Please provide a valid value for Data consent",
+ "placeholder": "",
+ "preValues": [],
+ "required": true,
+ "requiredErrorMessage": "Please confirm your data consent",
+ "settings": {
+ "acceptCopy": "Yes, I give permission to store and process my data.",
+ "showLabel": ""
+ },
+ "type": {
+ "id": "a72c9df9-3847-47cf-afb8-b86773fd12cd",
+ "name": "Data Consent"
+ }
+ },
+ {
+ "alias": "tickToAddMoreInfo",
+ "caption": "Tick to add more info",
+ "condition": {
+ "actionType": "Show",
+ "logicType": "All",
+ "rules": []
+ },
+ "helpText": "",
+ "id": "6ce0cf78-5102-47c1-85c6-9530d9e9c6a6",
+ "pattern": "",
+ "patternInvalidErrorMessage": "Please provide a valid value for Tick to add more info",
+ "placeholder": "",
+ "preValues": [],
+ "required": false,
+ "requiredErrorMessage": "Please provide a value for Tick to add more info",
+ "settings": {
+ "defaultValue": ""
+ },
+ "type": {
+ "id": "d5c0c390-ae9a-11de-a69e-666455d89593",
+ "name": "Checkbox"
+ }
+ },
+ {
+ "alias": "moreInfo",
+ "caption": "More info",
+ "condition": {
+ "actionType": "Show",
+ "logicType": "All",
+ "rules": [
+ {
+ "field": "6ce0cf78-5102-47c1-85c6-9530d9e9c6a6",
+ "operator": "Is",
+ "value": "on"
+ }
+ ]
+ },
+ "helpText": "",
+ "id": "5b4100ed-cc5e-4113-943c-ee5a8f4e448d",
+ "pattern": "",
+ "patternInvalidErrorMessage": "Please provide a valid value for More info",
+ "placeholder": "",
+ "preValues": [],
+ "required": false,
+ "requiredErrorMessage": "Please provide a value for More info",
+ "settings": {
+ "defaultValue": "",
+ "placeholder": "",
+ "showLabel": "",
+ "maximumLength": "",
+ "fieldType": "",
+ "autocompleteAttribute": ""
+ },
+ "type": {
+ "id": "3f92e01b-29e2-4a30-bf33-9df5580ed52c",
+ "name": "Short answer"
+ }
+ }
+ ],
+ "width": 0
+ }
+ ],
+ "id": "d677b96f-488d-4052-b00d-fb852b35e9c5"
+ }
+ ]
+ }
+ ],
+ "previousLabel": "Previous",
+ "showValidationSummary": false,
+ "submitLabel": "Submit"
+}
+```
+
+It's possible to define either a message displayed when a form is submitted, or a redirect to another website page.
+
+With the former, the output will be as above, and as shown in this extract:
+
+```json
+{
+ ...
+ "messageOnSubmit": "Thanks for submitting the form",
+ ...
+}
+```
+
+When a redirect is configured, details of the content ID and a route will be included, as follows:
+
+```json
+{
+ ...
+ "gotoPageOnSubmit": "eab72f13-b22e-46d5-b270-9c196e49a53b",
+ "gotoPageOnSubmitRoute": {
+ "path": "/contact-us/thanks/",
+ "startItem": {
+ "id": "ca4249ed-2b23-4337-b522-63cabe5587d1",
+ "path": "home"
+ }
+ },
+ ...
+}
+```
+
+### Submitting a Form Entry
+
+To submit a form entry, the following request can be made:
+
+```none
+POST /umbraco/forms/delivery/api/v1/entries/{id}
+```
+
+The POST request requires the Guid identifying the form.
+
+It also requires a `Content-Type` header of `application/json` and accepts a body as per this example:
+
+```json
+{
+ "values": {
+ "name": "Fred",
+ "email": "fred@test.com",
+ "comment": "Test",
+ "country": "it",
+ "favouriteColours": ["red", "green"],
+ "dataConsent": "on"
+ },
+ "contentId": "ca4249ed-2b23-4337-b522-63cabe5587d1",
+ "culture": "en-US",
+ "additionalData": {
+ "foo": "bar",
+ "baz": "buzz",
+ }
+}
+```
+
+The `values` collection consists of a set of name/value pairs, where the name is the alias of a form field. The value is the value of the submitted field, which can either be a string, or an array of strings. In this way we support fields that accept multiple values, such as checkbox lists.
+
+The `contentId` and `culture` parameters are optional. If provided they will be used to customize the response for the current page and language respectively.
+
+Similarly the `additionalData` dictionary is optional. This data is associated with the created record and made available within workflows.
+
+In the case of a validation error, a 422 "Unprocessable Entity" status code will be returned, along with a response similar to the following:
+
+```json
+{
+ "errors": {
+ "name": [
+ "Please provide a value for Name"
+ ]
+ },
+ "extensions": {},
+ "status": 422,
+ "title": "One or more validation errors occurred."
+}
+```
+
+A successful response will return a 202 "Accepted" status code.
+
+It will contain an object detailing the post-submission configured the form, for example:
+
+```json
+{
+ "gotoPageOnSubmit": "3cce2545-e3ac-44ec-bf55-a52cc5965db3",
+ "gotoPageOnSubmitRoute": {
+ "path": "/about-us/",
+ "startItem": {
+ "id": "ca4249ed-2b23-4337-b522-63cabe5587d1",
+ "path": "home"
+ }
+ },
+ "messageOnSubmit": "Thanks for your entry",
+ "messageOnSubmitIsHtml": false
+}
+```
+
+#### File Uploads
+
+The file upload field type is supported via the API for the rendering and submission of forms.
+
+When retrieving a form definition, some additional detail is provided for fields of this type to allow for the appropriate rendering of the form interface:
+
+```json
+ ...
+ "fields": [
+ {
+ "alias": "uploadAPicture",
+ ...
+ "fileUploadOptions": {
+ "allowAllUploadExtensions": false,
+ "allowedUploadExtensions": [
+ "png",
+ "jpg",
+ "gif"
+ ],
+ "allowMultipleFileUploads": false
+ },
+ }
+ ]
+ ...
+```
+
+When submitting a form, the value should be a JSON structure that provides a collection. Each item in the collection should contain the file name and the file contents as a base64 encoded data URL.
+
+```json
+{
+ "values": {
+ "uploadAPicture": [
+ {
+ "fileName": "mypic.jpg",
+ "fileContents": "data:image/png;base64,iVBORw..."
+ }
+ ]
+ },
+}
+```
+
+## Securing the API
+
+### Antiforgery Protection
+
+When posting forms in the traditional way, via a full page post back, an anti-forgery token is generated and validated. This provides protection against Cross-Site Request Forgery (CSRF) attacks.
+
+The same protection is available for forms submitted via AJAX techniques.
+
+In order to generate the token and provide it in the form post, the following code can be applied to the .cshtml template:
+
+```csharp
+@using Microsoft.AspNetCore.Antiforgery
+
+@inject IAntiforgery antiforgery
+
+@{
+ var tokenSet = antiforgery.GetAndStoreTokens(Context);
+}
+```
+
+When posting the form, the header value generated can be provided, where it will be validated server-side before accepting the request.
+
+```javascript
+ let response = await fetch("/umbraco/forms/delivery/api/v1/entries/" + formId, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "@tokenSet.HeaderName" : "@tokenSet.RequestToken"
+ },
+ body: JSON.stringify(data),
+ });
+```
+
+### API Key
+
+The antiforgery token security approach is valid when building a client-side integration with API calls made from the browser.
+
+Providing the token isn't possible though in other headless situations such as server-to-server requests. In these situations, an alternative approach to securing the API is available.
+
+Firstly, with server-to-server integrations you will want to disable the antiforgery token protection.
+
+This is done by setting the `Umbraco:Forms:Security:EnableAntiForgeryTokenForFormsApi` configuration key to a value of `false`.
+
+You should then configure an API key `Umbraco:Forms:Security:FormsApiKey`. This can be any string value, but it should be complex enough to resist being guessed by a brute force attack.
+
+With this in place any request to the Forms API will be rejected unless the configured value is provided in an HTTP header named `Api-Key`.
+
+## Rendering and Submitting forms with JavaScript
+
+For an illustrative example showing how a form can be rendered, validated and submitted using the API and vanilla JavaScript, please [see this gist](https://gist.github.com/AndyButland/9371175d6acf24a5307b053398f08448).
+
+Examples demonstrating how to handle a file upload and use reCAPTCHA fields are included.
+
+## Working with the CMS Content Delivery API
+
+The [Content Delivery API](https://docs.umbraco.com/umbraco-cms/v/12.latest/reference/content-delivery-api) provides headless capabilities within Umbraco by allowing you to retrieve content in JSON format.
+
+When retrieving content that contains an Umbraco Forms form picker, the output by default will consist of the ID of the selected form:
+
+```json
+{
+ "name": "Contact Us",
+ "route": {
+ "path": "/contact-us/",
+ "startItem": {
+ "id": "ca4249ed-2b23-4337-b522-63cabe5587d1",
+ "path": "home"
+ }
+ },
+ "id": "4a1f4198-e143-48ba-a0f5-1a7ef2df23aa",
+ "contentType": "contactPage",
+ "properties": {
+ "title": "Contact Us",
+ "contactForm": {
+ "id": "3623b232-9296-4bf0-b16c-57801dc4f296",
+ "form": null
+ }
+ }
+}
+```
+
+With [expanded output](https://docs.umbraco.com/umbraco-cms/v/12.latest/reference/content-delivery-api#output-expansion) for the property, the full details of the form will be available. The structure and content of the form representation will be exactly the same as that provided by the Forms API itself.
+
+```json
+{
+ "name": "Contact Us",
+ "route": {
+ "path": "/contact-us/",
+ "startItem": {
+ "id": "ca4249ed-2b23-4337-b522-63cabe5587d1",
+ "path": "home"
+ }
+ },
+ "id": "4a1f4198-e143-48ba-a0f5-1a7ef2df23aa",
+ "contentType": "contactPage",
+ "properties": {
+ "title": "Contact Us",
+ "contactForm": {
+ "id": "3623b232-9296-4bf0-b16c-57801dc4f296",
+ "form": {
+ "id": "3623b232-9296-4bf0-b16c-57801dc4f296",
+ "name": "Contact Form",
+ ...
+ "pages": [ ... ]
+ }
+ }
+ }
+}
+```
diff --git a/16/umbraco-forms/developer/block-list-labels.md b/16/umbraco-forms/developer/block-list-labels.md
new file mode 100644
index 00000000000..1e409fa0e43
--- /dev/null
+++ b/16/umbraco-forms/developer/block-list-labels.md
@@ -0,0 +1,2 @@
+# Block List Labels
+
diff --git a/16/umbraco-forms/developer/blocklistlabels.md b/16/umbraco-forms/developer/blocklistlabels.md
new file mode 100644
index 00000000000..c522271a943
--- /dev/null
+++ b/16/umbraco-forms/developer/blocklistlabels.md
@@ -0,0 +1,27 @@
+# Block List Labels
+
+When working with the Block List editor, [the editor experience is enhanced](https://docs.umbraco.com/umbraco-cms/fundamentals/backoffice/property-editors/built-in-umbraco-property-editors/block-editor/block-list-editor#editor-appearance) by defining a label for the appearance of the Block.
+
+These labels can use [Umbraco Flavored Markdown (UFM)](https://docs.umbraco.com/umbraco-cms/reference/umbraco-flavored-markdown).
+
+An option is available to display a form's name - `umbFormName`.
+
+It should be rendered as follows, with a reference to the property alias on the block element that uses a form picker.
+
+```
+{umbFormName: }
+```
+
+If you add a reference to a property containing a form to the block's label, it will render with the form's Id.
+
+For example, assuming a property containing a picked form with an alias of `contactForm`:
+
+```
+{=contactForm}
+```
+
+By using the markdown as follows, the form's name will be displayed instead.
+
+```
+{umbFormName: contactForm}
+```
diff --git a/16/umbraco-forms/developer/configuration/README.md b/16/umbraco-forms/developer/configuration/README.md
new file mode 100644
index 00000000000..051fce82a91
--- /dev/null
+++ b/16/umbraco-forms/developer/configuration/README.md
@@ -0,0 +1,572 @@
+---
+description: >-
+ In Umbraco Forms it's possible to customize the functionality with various
+ configuration values.
+---
+
+# Configuration
+
+With Umbraco Forms it's possible to customize the functionality with various configuration values.
+
+## Editing configuration values
+
+All configuration for Umbraco Forms is held in the `appSettings.json` file found at the root of your Umbraco website. If the configuration has been customized to use another source, then the same keys and values discussed in this article can be applied there.
+
+The convention for Umbraco configuration is to have package based options stored as a child structure below the `Umbraco` element, and as a sibling of `CMS`. Forms configuration follows this pattern, i.e.:
+
+```json
+{
+ ...
+ "Umbraco": {
+ "CMS": {
+ ...
+ },
+ "Forms": {
+ ...
+ }
+ }
+}
+```
+
+All configuration for Forms is optional. In other words, all values have defaults that will be applied if no configuration is available for a particular key.
+
+For illustration purposes, the following structure represents the full set of options for configuration of Forms, along with the default values. This will help when you need to provide a different setting to understand where it should be applied.
+
+```json
+ "Forms": {
+ "FormDesign": {
+ "DisableAutomaticAdditionOfDataConsentField": false,
+ "DisableDefaultWorkflow": false,
+ "MaxNumberOfColumnsInFormGroup": 12,
+ "DefaultTheme": "default",
+ "DefaultEmailTemplate": "Forms/Emails/Example-Template.cshtml",
+ "Defaults": {
+ "ManualApproval": false,
+ "DisableStylesheet": false,
+ "MarkFieldsIndicator": "NoIndicator",
+ "Indicator": "*",
+ "RequiredErrorMessage": "Please provide a value for {0}",
+ "InvalidErrorMessage": "Please provide a valid value for {0}",
+ "ShowValidationSummary": false,
+ "HideFieldValidationLabels": false,
+ "NextPageButtonLabel": "Next",
+ "PreviousPageButtonLabel": "Previous",
+ "SubmitButtonLabel": "Submit",
+ "MessageOnSubmit": "Thank you",
+ "StoreRecordsLocally": true,
+ "AutocompleteAttribute": "",
+ "DaysToRetainSubmittedRecordsFor": 0,
+ "DaysToRetainApprovedRecordsFor": 0,
+ "DaysToRetainRejectedRecordsFor": 0,
+ "ShowPagingOnMultiPageForms": "None",
+ "PagingDetailsFormat": "Page {0} of {1}",
+ "PageCaptionFormat": "Page {0}",
+ "ShowSummaryPageOnMultiPageForms": false,
+ "SummaryLabel": "Summary of Entry"
+ },
+ "RemoveProvidedFormTemplates": false,
+ "FormElementHtmlIdPrefix": "",
+ "SettingsCustomization": {
+ "DataSourceTypes": {},
+ "FieldTypes": {},
+ "PrevalueSourceTypes": {},
+ "WorkflowTypes": {},
+ },
+ "MandatoryFieldsetLegends": false
+ },
+ "Options": {
+ "IgnoreWorkFlowsOnEdit": "True",
+ "AllowEditableFormSubmissions": false,
+ "AppendQueryStringOnRedirectAfterFormSubmission": false,
+ "CultureToUseWhenParsingDatesForBackOffice": "",
+ "TriggerConditionsCheckOn": "change",
+ "ScheduledRecordDeletion": {
+ "Enabled": false,
+ "FirstRunTime": "",
+ "Period": "1.00:00:00"
+ },
+ "DisableRecordIndexing": false,
+ "EnableFormsApi": false,
+ "EnableRecordingOfIpWithFormSubmission": false,
+ "UseSemanticFieldsetRendering": false,
+ "DisableClientSideValidationDependencyCheck": false,
+ "DisableRelationTracking": false,
+ "TrackRenderedFormsStorageMethod": "HttpContextItems",
+ "EnableMultiPageFormSettings": true,
+ "EnableAdvancedValidationRules": false
+ },
+ "Security": {
+ "DisallowedFileUploadExtensions": "config,exe,dll,asp,aspx",
+ "AllowedFileUploadExtensions": "",
+ "EnableAntiForgeryToken": true,
+ "SavePlainTextPasswords": false,
+ "DisableFileUploadAccessProtection": false,
+ "DefaultUserAccessToNewForms": "Grant",
+ "ManageSecurityWithUserGroups": false,
+ "GrantAccessToNewFormsForUserGroups": "admin,editor",
+ "FormsApiKey": "",
+ "EnableAntiForgeryTokenForFormsApi": true,
+ },
+ "FieldTypes": {
+ "DatePicker": {
+ "DatePickerYearRange": 10,
+ "DatePickerFormat": "LL",
+ "DatePickerFormatForValidation": ""
+ },
+ "Recaptcha2": {
+ "PublicKey": "",
+ "PrivateKey": ""
+ },
+ "Recaptcha3": {
+ "SiteKey": "",
+ "PrivateKey": "",
+ "Domain": "Google",
+ "VerificationUrl": "https://www.google.com/recaptcha/api/siteverify",
+ "ShowFieldValidation": true
+ },
+ "RichText": {
+ "DataTypeId": "ca90c950-0aff-4e72-b976-a30b1ac57dad"
+ },
+ "TitleAndDescription": {
+ "AllowUnsafeHtmlRendering": false
+ }
+ }
+ }
+```
+
+## Form design configuration
+
+### DisableAutomaticAdditionOfDataConsentField
+
+This configuration value expects a `true` or `false` value and can be used to disable the feature where all new forms are provided with a default "Consent for storing submitted data" field on creation. Defaults to `false`.
+
+### DisableDefaultWorkflow
+
+This configuration value expects a `true` or `false` value and can be used to toggle if new forms that are created adds an email workflow to send the result of the form to the current user who created the form. Defaults to `false`.
+
+### MaxNumberOfColumnsInFormGroup
+
+This setting controls the maximum number of columns that can be created by editors when they configure groups within a form. The default value used if the setting value is not provided is 12.
+
+### DefaultTheme
+
+This setting allows you to configure the name of the theme to use when an editor has not specifically selected one for a form. If empty or missing, the default value of "default" is used. If a custom default theme is configured, it will be used for rendering forms where the requested file exists, and where not, will fall back to the out of the box default theme.
+
+### DefaultEmailTemplate
+
+When creating an empty form, a single workflow is added that will send an email to the current user's address. By default, the template shipped with Umbraco Forms is available at `Forms/Emails/Example-Template.cshtml` is used.
+
+If you have created a custom template and would like to use that as the default instead, you can set the path here using this configuration setting.
+
+### RemoveProvidedFormTemplates
+
+Similarly, the provided form templates available from the form creation dialog can be removed from selection. To do this, set this configuration value to `true`.
+
+### FormElementHtmlIdPrefix
+
+By default the value of HTML `id` attribute rendered for fieldsets and fields using the default theme is the GUID associated with the form element. Although [this is valid](https://developer.mozilla.org/en-US/docs/Web/HTML/Global\_attributes/id), some browsers, particularly Safari, may report issues with this if the identifier begins with a number. To avoid such issues, the attribute values can be prefixed with the value provided in this configuration element.
+
+For example, providing a value of `"f_"` will apply a prefix of "f\_" to each fieldset and field `id` attribute.
+
+### SettingsCustomization
+
+Forms introduced the ability to configure settings for the field, workflow, data source, and prevalue sources. The default behavior, when a new field or workflow is added to a form, is for each setting to be empty. The values are then completed by the editor. All settings defined on the type are displayed for entry.
+
+In some situations, you may want to hide certain settings from entry, so they always take an empty value. In others, you may want to provide a default value that the editor can accept or amend. And lastly, you may have a requirement for a fixed, non-empty value, that's enforced by the organization and not editable. Each of these scenarios can be supported by this configuration setting.
+
+It consists of four dictionaries, one for each type:
+
+* `DataSourceTypes`
+* `FieldTypes`
+* `PrevalueSourceTypes`
+* `WorkflowTypes`
+
+Each dictionary can be identified using the GUID or alias of the type as the key. The value is set to the following structure that contains three settings:
+
+```json
+{
+ "IsHidden": true|false,
+ "DefaultValue": "",
+ "IsReadOnly": true|false
+}
+```
+
+* `IsHidden` - if provided and set to true the setting will be hidden and will always have an empty value.
+* `DefaultValue` - if provided the value will be pre-filled when a type using it is created.
+* `IsReadOnly` - used in conjunction with the above, if set the field won't be editable and hence whatever is set as the `DefaultValue` won't be able to be changed. If set to false (or omitted) the editor can change the value from the default.
+
+In this example, the sender address field on a workflow for sending emails can be hidden, such that the system configured value is always used:
+
+```json
+ "SettingsCustomization": {
+ "WorkflowTypes": {
+ "sendEmailWithRazorTemplate": {
+ "SenderEmail": {
+ "IsHidden": true
+ }
+ }
+ },
+ }
+```
+
+Here an organization-approved reCAPTCHA score threshold is defined, that can't be changed by editors:
+
+```json
+ "SettingsCustomization": {
+ "FieldTypes": {
+ "recaptcha3": {
+ "ScoreThreshold": {
+ "DefaultValue": "0.8",
+ "IsReadOnly": true
+ }
+ }
+ },
+ }
+```
+
+In order to configure this setting, you will need to know the GUID or alias for the type and the property name for each setting. You can find [these values for the built-in Forms types her](type-details.md)e.
+
+Take care to not hide any settings that are required for the particular field or workflow type (for example, the `Subject` field for email workflows). If you do that, the item will fail validation when an editor tries to create it.
+
+The default value and read-only settings apply to most setting types. There is an exception for complex ones where a default string value isn't appropriate. An example of one of these is the field mapper used in the "Send to URL" workflow.
+
+### MandatoryFieldsetLegends
+
+When creating a form with Umbraco Forms, adding captions to the groups for fields is optional. To follow accessibility best practices, these fields should be completed. When they are, the group of fields are presented within a `` element that has a populated ``.
+
+If you want to ensure form creators always have to provide a caption, you can set the value of this setting to `true`.
+
+### Form default settings configuration
+
+The following configured values are applied to all forms as they are created. They can then be amended on a per-form basis via the Umbraco backoffice.
+
+Once the form has been created, the values are set explicitly on the form, so subsequent changes to the defaults in configuration won't change the settings used on existing forms.
+
+#### ManualApproval
+
+This setting needs to be a `true` or `false` value and will allow you to toggle if a form allows submissions to be post moderated. Most use cases are for publicly shown entries such as blog post comments or submissions for a social campaign. Defaults to `false`.
+
+#### DisableStylesheet
+
+This setting needs to be a `true` or `false` value and will allow you to toggle if the form will include some default styling with the Umbraco Forms CSS stylesheet. Defaults to `false`.
+
+#### MarkFieldsIndicator
+
+This setting can have the following values to allow you to toggle the mode of marking mandatory or optional fields:
+
+* `NoIndicator` (default)
+* `MarkMandatoryFields`
+* `MarkOptionalFields`
+
+#### Indicator
+
+This setting is used to mark the mandatory or optional fields based on the setting above. By default this is an asterisk `*`.
+
+#### RequiredErrorMessage
+
+This allows you to configure the required error validation message. By default this is `Please provide a value for {0}` where the `{0}` is used to replace the name of the field that is required.
+
+#### InvalidErrorMessage
+
+This allows you to configure the invalid error validation message. By default this is `Please provide a valid value for {0}` where the `{0}` is used to replace the name of the field that is invalid.
+
+#### ShowValidationSummary
+
+This setting needs to be a `true` or `false` value and will allow you to toggle if the form will display all form validation error messages in a validation summary together. Defaults to `false`.
+
+#### HideFieldValidationLabels
+
+This setting needs to be a `true` or `false` value and will allow you to toggle if the form will show inline validation error messages next to the form field that is invalid. Defaults to `false`.
+
+#### NextPageButtonLabel, PreviousPageButtonLabel, SubmitButtonLabel
+
+These settings configure the default next, previous, and submit button labels. By default, these are `Next`, `Previous`, and `Submit` respectively. These labels can be amended on a form-by-form basis via the form's **Settings** section.
+
+#### MessageOnSubmit
+
+This allows you to configure what text is displayed when a form is submitted and is not being redirected to a different content node. Defaults to `Thank you`.
+
+#### StoreRecordsLocally
+
+This setting needs to be a `True` or `False` value and will allow you to toggle if form submission data should be stored in the Umbraco Forms database tables. By default this is set to `True`.
+
+#### AutocompleteAttribute
+
+This setting provides a value to be used for the `autocomplete` attribute for newly created forms. By default the value is empty, but can be set to `on` or `off` to have that value applied as the attribute value used when rendering the form.
+
+#### DaysToRetainSubmittedRecordsFor
+
+Introduced in 10.2, this setting controls the initial value of the number of days to retain form submission records for newly created forms. By default the value is 0, which means records will not be deleted at any time and are retained forever.
+
+If set to a positive number, a date value calculated by taking away the number of days configured from the current date is found. Records in the 'submitted' state, that are older than this date, will be flagged for removal.
+
+#### DaysToRetainApprovedRecordsFor
+
+Applies as per `DaysToRetainSubmittedRecordsFor` but for records in the 'approved' state.
+
+#### DaysToRetainRejectedRecordsFor
+
+Applies as per `DaysToRetainSubmittedRecordsFor` but for records in the 'rejected' state.
+
+### ShowPagingOnMultiPageForms
+
+Defines whether and where paging details are displayed for multi-page forms.
+
+### PagingDetailsFormat
+
+Defines the paging details format for multi-page forms.
+
+### PageCaptionFormat
+
+Defines the page caption format for multi-page forms.
+
+### ShowSummaryPageOnMultiPageForms
+
+Defines whether summary pages are on by default for multi-page forms.
+
+### SummaryLabel
+
+Defines the default summary label for multi-page forms.
+
+## Package options configuration
+
+### IgnoreWorkFlowsOnEdit
+
+This configuration expects a `True` or `False` string value, or a comma-separated list of form names, and allows you to toggle if a form submission is edited again, that the workflows on the form will re-fire after an update to the form submission. This is used in conjunction with the `AllowEditableFormSubmissions` configuration value. Defaults to `True`.
+
+### AllowEditableFormSubmissions
+
+This configuration value expects a `true` or `false` value and can be used to toggle the functionality to allow a form submission to be editable and re-submitted. When the value is set to `true` it allows Form Submissions to be edited using the following querystring for the page containing the form on the site. `?recordId=GUID` Replace `GUID` with the GUID of the form submission. Defaults to `false`.
+
+{% hint style="info" %}
+There was a typo in this setting where it had been named as `AllowEditableFormSubmissions`. This is the name that needs to be used in configuration for Forms 9. In Forms 10 this was be corrected to the now documented value of `AllowEditableFormSubmissions`.
+{% endhint %}
+
+{% hint style="warning" %}
+Enable this feature ONLY if you understand the security implications.
+{% endhint %}
+
+### AppendQueryStringOnRedirectAfterFormSubmission
+
+When redirecting following a form submission, a `TempData` value is set that is used to ensure the submission message is displayed rather than the form itself. In certain situations, such as hosting pages with forms in IFRAMEs from other websites, this value is not persisted between requests.
+
+By setting the following value to True, a querystring value of `formSubmitted=`, will be used to indicate a form submitted on the previous request.
+
+### CultureToUseWhenParsingDatesForBackOffice
+
+This setting has been added to help resolve an issue with multi-lingual setups.
+
+When Umbraco Forms stores data for a record, it saves the values submitted for each field into a dedicated table for each type (string, date etc.). It also saves a second copy of the record in a JSON structure which is more suitable for fast look-up and display in the backoffice. Date values are serialized using the culture used by the front-end website when the form entry is stored.
+
+When displaying the data in the backoffice, the date value needs to be parsed back into an actual date object for formatting. And this can cause a problem if the backoffice user is using a different language, and hence culture setting, than that used when the value was stored.
+
+The culture used when storing the form entry is recorded, thus we can ensure the correct value is used when parsing the date. However, this doesn't help for historically stored records. To at least partially mitigate the problem, when you have editors using different languages to a single language presented on the website front-end, you can set this value to match the culture code used on the website. This ensures the date fields in the backoffice are correctly presented.
+
+Taking an example of a website globalization culture code setting of "en-US" (and a date format of `m/d/y`), but an editor uses "en-GB" (which formats dates as of `d/m/y`). By setting the value of this configuration key to "en-US", you can ensure that the culture when parsing dates for presentation in the backoffice will match that used when the value was stored.
+
+If no value is set, and no culture value was stored alongside the form entry, the culture based on the language associated with the current backoffice user will be used.
+
+### TriggerConditionsCheckOn
+
+This configuration setting provides control over the client-side event used to trigger conditions. The `change` event is the default used if this setting is empty. It can also be set to a value of `input`. The main difference seen here relates to text fields, with the "input" event firing on each key press, and the "change" only when the field loses focus.
+
+### ScheduledRecordDeletion
+
+Scheduled deletion of records older than a specified number of days. It uses a background task to run the cleanup operation, which can be customized with the following settings.
+
+#### Enabled
+
+By default this value is `false` and no data will be removed. Even if forms are configured to have submitted data cleaned up, no records will be deleted. A note will be displayed in the backoffice indicating this status.
+
+Set to `true` to enabled the background task.
+
+#### FirstRunTime
+
+This will configure when the record deletion process will run for the first time. If the value is not configured the health checks will run after a short delay following the website start. The value is specified as a string in crontab format. For example, a value of `"* 4 * * *"` will first run the operation at 4 a.m.
+
+#### Period
+
+Defines how often the record deletion process will run. The default value is `1.00:00:00` which is equivalent to once every 24 hours. Shorter or longer periods can be set using different datetime strings.
+
+### DisableRecordIndexing
+
+Set this value to `true` to disable the default behavior of indexing the form submissions into the Examine index.
+
+If indexing has already occurred, you will still need to manually remove the files (found in `App_Data\TEMP\ExamineIndexes\UmbracoFormsRecords`). They will be recreated if indexing is subsequently re-enabled.
+
+### EnableFormsApi
+
+Set this value to `true` to enable the Forms API supporting headless and AJAX forms.
+
+### EnableRecordingOfIpWithFormSubmission
+
+By default, the user's IP address is not recorded when a form is submitted and stored in the `UFRecords` database table.
+
+To include this information in the saved data, set this value to `true`.
+
+If recording IPs and your site is behind a proxy, load balancer or CDN, we recommend using [ASP.NET's forwarded headers middleware](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-7.0) to ensure the correct value for the client IP is resolved.
+
+### UseSemanticFieldsetRendering
+
+In Forms 12.1 amends were made to the default theme for Forms that improved accessibility. Specifically we provide the option to use alternative markup for rendering checkbox and radio button lists. These use the more semantically correct `fieldset` and `legend` elements, instead of the previously used `div` and `label`.
+
+Although this semantic markup is preferred, it could be a presentational breaking change for those styling the default theme. As such we have made this markup improvement optional. You can opt into using it by setting this configuration value to `true`.
+
+In Umbraco 13 this configuration option will be removed and the semantic rendering made the only option.
+
+### DisableClientSideValidationDependencyCheck
+
+When a form is rendered on the front-end website, a check is run to ensure that client-side validation framework is available and registered.
+
+You can disable this check by setting the value of this configuration key to `true`.
+
+If you are rendering your forms dependency scripts using the `async` attribute, you will need to disable this check.
+
+### DisableRelationTracking
+
+Forms will by default track relations between forms and the content pages they are used on. This allows editors to see where forms are being used in their Umbraco website.
+
+If you would like to enable this feature, you can set the value of this setting to `true`.
+
+## TrackRenderedFormsStorageMethod
+
+Forms tracks the forms rendered on a page in order that the associated scripts can be placed in a different location within the HTML. Usually this is used to [render the scripts](../rendering-scripts.md) at the bottom of the page.
+
+By default, `HttpContext.Items` is used as the storage mechanism for this tracking.
+
+You can optionally revert to the legacy behavior of using `TempData` by changing this setting from the default of `HttpContextItems` to `TempData`.
+
+## EnableMultiPageFormSettings
+
+This setting determines whether [multi-page form settings](../../editor/creating-a-form/form-settings.md#multi-page-forms) are available to editors.
+
+By default the value is `true`. To disable the feature, set the value to `false`.
+
+## EnableAdvancedValidationRules
+
+This setting determines whether [advanced form validation rules](../../editor/creating-a-form/form-advanced.md) are available to editors.
+
+By default, the value is `false`. This is partly because the feature is only considered for "power users", comfortable with crafting rules using the required JSON syntax. And partly as validating the rules on the client requires an additional front-end dependency.
+
+To make the feature available to editors and include the dependency when using `@Html.RenderUmbracoFormDependencies(Url)`, set the value to `true`.
+
+
+## Security configuration
+
+### DisallowedFileUploadExtensions
+
+When using the File Upload field in a form, editors can choose which file extensions they want to accept. When an image is expected, they can for example specify that only `.jpg` or `.png` files are uploaded.
+
+There are certain file extensions that in almost all cases should never be allowed, which are held in this configuration value. This means that even if an editor has selected to allow all files, any files that match the extensions listed here will be blocked.
+
+By default, .NET related code files like `.config` and `.aspx` are included in this deny list. You can add or - if you are sure - remove values from this list to meet your needs.
+
+### AllowedFileUploadExtensions
+
+For further control, an "allow list" of extension can be provided via this setting. If provided, only the extensions entered as a comma separated list here will be accepted in file uploads through forms.
+
+### EnableAntiForgeryToken
+
+This setting needs to be a `true` or `false` value and will enable the ASP.NET Anti Forgery Token and we recommend that you enable this option. Defaults to `true`.
+
+In certain circumstances, including hosting pages with forms in IFRAMEs from other websites, this may need to be set to `false`.
+
+### SavePlainTextPasswords
+
+This setting needs to be a `true` or `false` value and controls whether password fields provided in forms will be saved to the database. Defaults to `false`.
+
+### DisableFileUploadAccessProtection
+
+Protection was added to uploaded files to prevent users from accessing them if they aren't logged into the backoffice and have permission to manage the form for which the file was submitted. As a policy of being "secure by default", the out of the box behavior is that this access protection is in place.
+
+If for any reason you need to revert to the previous behavior, or have other reasons where you want to permit unauthenticated users from accessing the files, you can turn off this protection by setting this configuration value to `true`.
+
+### DefaultUserAccessToNewForms
+
+This setting was added to add control over access to new forms. The default behavior is for all users to be granted access to newly created forms. To amend that to deny access, the setting can be updated to a value of `Deny`. A value of `Grant` or configuration with the setting absent preserves the default behavior.
+
+### ManageSecurityWithUserGroups
+
+Ability to administer access to Umbraco Forms using Umbraco's user groups. This can be used instead or in addition to the legacy administration which is at the level of the individual user. Set this option to `true` to enable the user group permission management functionality.
+
+### GrantAccessToNewFormsForUserGroups
+
+This setting takes a comma-separated list of user group aliases which will be granted access automatically to newly created forms. This setting only takes effect when `ManageSecurityWithUserGroups` is set to `true`.
+
+There are two "special" values that can be applied within or instead of the comma-separated list.
+
+A value of `all` will give access to the form to all user groups.
+
+A value of `form-creator` will give access to all the user groups that the user who created the form is part of.
+
+### FormsApiKey and EnableAntiForgeryTokenForFormsApi
+
+Available from Forms 10.2.1, the `FormsApiKey` configuration setting can be used to secure the Forms Headless API in server-to-server integrations. When set, API calls will be rejected unless the value of this setting is provided in an HTTP header.
+
+Setting the value of `EnableAntiForgeryTokenForFormsApi` to `false` will disable the anti-forgery protection for the Forms Headless/AJAX API. You need to do this for server-to-server integrations where it's not possible to provide a valid anti-forgery token in the request.
+
+For more information, see the [Headless/AJAX Forms](../ajaxforms.md) article.
+
+## Field type specific configuration
+
+### Date picker field type configuration
+
+#### DatePickerYearRange
+
+This setting is used to configure the Date Picker form field range of years that is available in the date picker. By default this is a small range of 10 years.
+
+#### DatePickerFormat
+
+A custom date format can be provided in [momentjs format](https://momentjscom.readthedocs.io/en/latest/moment/01-parsing/03-string-format/) if you want to override the default.
+
+#### DatePickerFormatForValidation
+
+If a custom date format is provided it will be used on the client side. A matching string in [C# date format](https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings) should be provided, so that server-side validation will match the expected format of the entry.
+
+### reCAPTCHA v2 field type configuration
+
+#### PublicKey & PrivateKey
+
+Both of these configuration values are needed in order to use the "_Recaptcha2_" field type implementing legacy ReCaptcha V2 from Google. You can obtain both of these values after signing up to create a ReCaptcha key here - [https://www.google.com/recaptcha/admin](https://www.google.com/recaptcha/admin)
+
+Google has renamed these recently and the `Site Key` refers to `RecaptchaPublicKey` and `Secret Key` is to be used for `RecaptchaPrivateKey`
+
+### reCAPTCHA v3 field type configuration
+
+#### SiteKey & PrivateKey
+
+Both of these configuration values are needed in order to use the "_reCAPTCHA V3 with Score_" field type implementing ReCaptcha V3 from Google.
+
+You can obtain both of these values after signing up to create a ReCaptcha key here: [https://www.google.com/recaptcha/admin](https://www.google.com/recaptcha/admin).
+
+#### Domain
+
+This setting defines the domain from which the client-side assets for using the reCAPTCHA service are requested.
+
+Valid options are `Google` (the default) or `Recaptcha`. You may want to use the latter for control of which domains are setting cookies on your site. [Read more at the reCAPTCHA documentation](https://developers.google.com/recaptcha/docs/faq#does-recaptcha-use-cookies).
+
+#### VerificationUrl
+
+By default, the server-side validation of the reCAPTCHA response is sent to Google's servers at `https://www.google.com/recaptcha/api/siteverify`.
+
+Some customers with a locked-down production environment cannot configure the firewall to allow these requests and instead use a proxy server. They can use this setting to configure the URL to their proxy server, which will relay the request to and response from Google.
+
+#### ShowFieldValidation
+
+The validation message returned from a failed reCAPTCHA 3 request will be displayed in the form level validation summary and alongside the field.
+
+To remove rendering at the field level, set this value to `false`.
+
+### Rich text field type configuration
+
+#### DataTypeId
+
+Sets the Data Type Guid to use to obtain the configuration for the rich text field type. If the setting is absent, the value of the default rich text Data Type created by Umbraco on a new install is used.
+
+### Title and description field type configuration
+
+#### AllowUnsafeHtmlRendering
+
+When using the "title and description" field type, if editors provide HTML in the "description" field it will be encoded when rendering on the website.
+
+If you understand the risks and want to allow HTML to be displayed, you can set this value to `false`.
diff --git a/16/umbraco-forms/developer/configuration/type-details.md b/16/umbraco-forms/developer/configuration/type-details.md
new file mode 100644
index 00000000000..e8c869dfa58
--- /dev/null
+++ b/16/umbraco-forms/developer/configuration/type-details.md
@@ -0,0 +1,526 @@
+---
+description: "Provides details of the built-in provider types available with Umbraco Forms"
+---
+
+# Forms Provider Type Details
+
+This page provides some details of the provider types available in Umbraco Forms.
+
+The intention is to be able to make available details such as IDs, aliases and property names, that may be necessary when configuring the product.
+
+## Field Types
+
+
+
+Checkbox
+
+**ID:** `D5C0C390-AE9A-11DE-A69E-666455D89593`
+
+**Alias:** `checkbox`
+
+**Settings:**
+
+* `Caption`
+* `DefaultValue`
+* `ShowLabel`
+
+
+
+
+
+Data Consent
+
+**ID:** `A72C9DF9-3847-47CF-AFB8-B86773FD12CD`
+
+**Alias:** `dataConsent`
+
+**Settings:**
+
+* `AcceptCopy`
+* `ShowLabel`
+
+
+
+
+
+Date
+
+**ID:** `F8B4C3B8-AF28-11DE-9DD8-EF5956D89593`
+
+**Alias:** `date`
+
+**Settings:**
+
+* `Placeholder`
+
+
+
+
+
+Dropdown List
+
+**ID:** `0DD29D42-A6A5-11DE-A2F2-222256D89593`
+
+**Alias:** `dropdown`
+
+**Settings:**
+
+* `DefaultValue`
+* `AllowMultipleSelections`
+* `ShowLabel`
+* `AutocompleteAttribute`
+* `SelectPrompt`
+
+
+
+
+
+File Upload
+
+**ID:** `84A17CF8-B711-46a6-9840-0E4A072AD000`
+
+**Alias:** `fileUpload`
+
+**Settings:**
+
+* `SelectedFilesListHeading`
+
+
+
+
+
+Long Answer
+
+**ID:** `023F09AC-1445-4bcb-B8FA-AB49F33BD046`
+
+**Alias:** `longAnswer`
+
+**Settings:**
+
+* `DefaultValue`
+* `Placeholder`
+* `ShowLabel`
+* `AutocompleteAttribute`
+* `NumberOfRows`
+* `MaximumLength`
+
+
+
+
+
+Hidden Field
+
+**ID:** `DA206CAE-1C52-434E-B21A-4A7C198AF877`
+
+**Alias:** `hidden`
+
+**Settings:**
+
+* `DefaultValue`
+
+
+
+
+
+Multiple Choice
+
+**ID:** `FAB43F20-A6BF-11DE-A28F-9B5755D89593`
+
+**Alias:** `multipleChoice`
+
+**Settings:**
+
+* `DisplayLayout`
+* `DefaultValue`
+* `ShowLabel`
+
+
+
+
+
+Password
+
+**ID:** `FB37BC60-D41E-11DE-AEAE-37C155D89593`
+
+**Alias:** `password`
+
+**Settings:**
+
+* `Placeholder`
+
+
+
+
+
+reCAPTCHA 2
+
+**ID:** `B69DEAEB-ED75-4DC9-BFB8-D036BF9D3730`
+
+**Alias:** `recaptcha2`
+
+**Settings:**
+
+* `Theme`
+* `Size`
+* `ErrorMessage`
+
+
+
+
+
+reCAPTCHA 3
+
+**ID:** `663AA19B-423D-4F38-A1D6-C840C926EF86`
+
+**Alias:** `recaptcha3`
+
+**Settings:**
+
+* `ScoreThreshold`
+* `ErrorMessage`
+* `SaveScore`
+
+
+
+
+
+Rich Text
+
+**ID:** `1F8D45F8-76E6-4550-A0F5-9637B8454619`
+
+**Alias:** `richText`
+
+**Settings:**
+
+* `Html`
+* `ShowLabel`
+
+
+
+
+
+Single Choice
+
+**ID:** `903DF9B0-A78C-11DE-9FC1-DB7A56D89593`
+
+**Alias:** `singleChoice`
+
+**Settings:**
+
+* `DisplayLayout`
+* `DefaultValue`
+* `ShowLabel`
+
+
+
+
+
+Short Answer
+
+**ID:** `3F92E01B-29E2-4a30-BF33-9DF5580ED52C`
+
+**Alias:** `shortAnswer`
+
+**Settings:**
+
+* `DefaultValue`
+* `Placeholder`
+* `ShowLabel`
+* `MaximumLength`
+* `FieldType`
+* `AutocompleteAttribute`
+
+
+
+
+
+Title and Description
+
+**ID:** `e3fbf6c4-f46c-495e-aff8-4b3c227b4a98`
+
+**Alias:** `titleAndDescription`
+
+**Settings:**
+
+* `CaptionTag`
+* `Caption`
+* `BodyText`
+* `ShowLabel`
+
+
+
+## Workflow Types
+
+
+
+Change Record State
+
+**ID:** `4C40A092-0CB5-481d-96A7-A02D8E7CDB2F`
+
+**Alias:** `changeRecordState`
+
+**Settings:**
+
+* `Words`
+* `Action`
+
+
+
+
+
+Post as XML
+
+**ID:** `470EEB3A-CB15-4b08-9FC0-A2F091583332`
+
+**Alias:** `postAsXml`
+
+**Settings:**
+
+* `Url`
+* `Method`
+* `XsltFile`
+* `Fields`
+* `Username`
+* `Password`
+
+
+
+
+
+Save As Umbraco Content Node
+
+**ID:** `89FB1E31-9F36-4e08-9D1B-AF1180D340DB`
+
+**Alias:** `saveAsUmbracoContentNode`
+
+**Settings:**
+
+* `Fields`
+* `Publish`
+* `RootNode`
+
+
+
+
+
+Save As XML File
+
+**ID:** `9CC5854D-61A2-48f6-9F4A-8F3BDFAFB521`
+
+**Alias:** `saveAsAnXmlFile`
+
+**Settings:**
+
+* `Path`
+* `Extension`
+* `XsltFile`
+
+
+
+
+
+Send Email
+
+**ID:** `E96BADD7-05BE-4978-B8D9-B3D733DE70A5`
+
+**Alias:** `sendEmail`
+
+**Settings:**
+
+* `Email`
+* `CcEmail`
+* `BccEmail`
+* `SenderEmail`
+* `ReplyToEmail`
+* `Subject`
+* `Message`
+* `Attachment`
+
+
+
+
+
+Send Email With Razor Template
+
+**ID:** `17c61629-d984-4e86-b43b-a8407b3efea9`
+
+**Alias:** `sendEmailWithRazorTemplate`
+
+**Settings:**
+
+* `Email`
+* `CcEmail`
+* `BccEmail`
+* `SenderEmail`
+* `ReplyToEmail`
+* `Subject`
+* `RazorViewFilePath`
+* `Attachment`
+
+
+
+
+
+Send Email With Extensible Stylesheet Language Transformations (XSLT) Template
+
+**ID:** `616edfeb-badf-414b-89dc-d8655eb85998`
+
+**Alias:** `sendEmailWithXsltTemplate`
+
+**Settings:**
+
+* `Email`
+* `CcEmail`
+* `BccEmail`
+* `SenderEmail`
+* `ReplyToEmail`
+* `Subject`
+* `XsltFile`
+
+
+
+
+
+Send Form To URL
+
+**ID:** `FD02C929-4E7D-4f90-B9FA-13D074A76688`
+
+**Alias:** `sendFormToUrl`
+
+**Settings:**
+
+* `Url`
+* `Method`
+* `StandardFields`
+* `Fields`
+* `Username`
+* `Password`
+
+
+
+
+
+Slack
+
+**ID:** `bc52ab28-d3ff-42ee-af75-a5d49be83040`
+
+**Alias:** `slack`
+
+**Settings:**
+
+* `WebhookUrl`
+
+
+
+
+
+Slack (Legacy)
+
+**ID:** `ccbfb0d5-adaa-4729-8b4c-4bb439dc0202`
+
+**Alias:** `slackLegacy`
+
+**Settings:**
+
+* `Token`
+* `Channel`
+* `Username`
+* `AvatarUrl`
+
+
+
+## Prevalue Source Types
+
+
+
+Datasource
+
+**ID:** `cc9f9b2a-a746-11de-9e17-681b56d89593`
+
+**Alias:** `dataSource`
+
+
+
+
+
+Get Values From Text File
+
+**ID:** `35C2053E-CBF7-4793-B27C-6E97B7671A2D`
+
+**Alias:** `getValuesFromTextFile`
+
+**Settings:**
+
+* `TextFile`
+
+
+
+
+
+SQL Database
+
+**ID:** `F1F5BD4D-E6AE-44ed-86CB-97661E4660B2`
+
+**Alias:** `sqlDatabase`
+
+**Settings:**
+
+* `Connection`
+* `ConnectionString`
+* `Table`
+* `KeyColumn`
+* `ValueColumn`
+* `CaptionColumn`
+
+
+
+
+
+Umbraco Datatype Prevalues
+
+**ID:** `EA773CAF-FEF2-491B-B5B7-6A3552B1A0E2`
+
+**Alias:** `umbracoDataTypePreValues`
+
+**Settings:**
+
+* `DataTypeId`
+
+
+
+
+
+Umbraco Documents
+
+**ID:** `de996870-c45a-11de-8a39-0800200c9a66`
+
+**Alias:** `umbracoDocuments`
+
+**Settings:**
+
+* `RootNode`
+* `UseCurrentPage`
+* `DocType`
+* `ValueField`
+* `CaptionField`
+* `ListGrandChildren`
+* `OrderBy`
+
+
+
+## Data Source Types
+
+
+
+SQL Database
+
+**ID:** `F19506F3-EFEA-4b13-A308-89348F69DF91`
+
+**Alias:** `sqlDatabase`
+
+**Settings:**
+
+* `Connection`
+* `Table`
+
+
diff --git a/16/umbraco-forms/developer/custom-markup.md b/16/umbraco-forms/developer/custom-markup.md
new file mode 100644
index 00000000000..65d29f0059a
--- /dev/null
+++ b/16/umbraco-forms/developer/custom-markup.md
@@ -0,0 +1,86 @@
+---
+description: >-
+ This article teaches you how to customize how your Umbraco Forms are
+ outputted.
+---
+
+# Custom Markup
+
+With Umbraco Forms, it is possible to customize the output markup of a Form, which means you have complete control over what Forms will display.
+
+{% hint style="warning" %}
+We recommend using [Themes](themes.md) to customize your Forms. This will ensure that nothing is overwritten when you upgrade Forms to a newer version.
+{% endhint %}
+
+## Customizing the Default Views
+
+The razor macro uses some razor views to output the Form:
+
+* 1 view for each fieldtype
+* 1 view for the scripts
+* 1 view for the rest of the Form
+
+You can find the default views in the `~\Views\Partials\Forms\Themes\default` folder.
+
+To avoid your custom changes to the default views from being overwritten, you need to copy the view you want to customize into your theme folder, e.g. `~\Views\Partials\Forms\Themes\YourTheme`, and edit the file in `YourTheme` folder.
+
+### Form.cshtml
+
+This is the main view responsible for rendering the Form markup.
+
+The view is separated into two parts, one is the actual Form and the other will be shown if the Form is submitted.
+
+This view can be customized, if you do so it will be customized for all your Forms.
+
+### Script.cshtml
+
+This view renders the JavaScript that will take care of the conditional logic, customization won't be needed here.
+
+### FieldType.\*.cshtml
+
+The rest of the views start with FieldType, like `FieldType.Textfield.cshtml` and those will output the fields. There is a view for each default fieldtype like _textfield_, _textarea_, _checkbox_, etc)
+
+Contents of the `FieldType.Textfield.cshtml` view (from the default theme):
+
+```csharp
+@model Umbraco.Forms.Mvc.Models.FieldViewModel
+@using Umbraco.Forms.Mvc
+
+ placeholder="@Model.PlaceholderText"}}
+ @{if(Model.Mandatory || Model.Validate){data-val="true" }}
+ @{if (Model.Mandatory) { data-val-required="@Model.RequiredErrorMessage" }}
+ @{if (Model.Validate) { data-val-regex="@Model.InvalidErrorMessage" data-val-regex-pattern="@Html.Raw(Model.Regex)" }}
+/>
+```
+
+Umbraco Forms uses ASP.NET Unobtrusive Validation which is why you see attributes like `data-val` and `data-val-required`.
+
+This can be customized but it's important to keep the ID of the control to `@Model.Id` since that is used to match the value to the Form field. For fields that are conditionally hidden, without an ID of `@Model.Id` the control won't be shown when the conditions to show the field are met. An ID needs to be added to text controls such as headings and paragraphs.
+
+The view model for the partial view for a field is `FieldViewModel`. This defines properties that may be useful when creating new themes or custom fields, some of which are shown in the code samples above. Others include:
+
+* `AdditionalSettings` - a dictionary of the settings keys and values populated for the form field. These can be retrieved in typed form by key using e.g. `Model.GetSettingValue("MaximumLength", 255);`.
+
+The following are available on the model but only populated for fields that support file upload:
+
+* `AllowAllUploadExtensions`- a boolean indicating whether all file extensions are permitted for upload.
+* `AllowedUploadExtensions`- a collection of strings indicating the file extensions that are permitted for upload.
+* `AllowMultipleFileUploads`- a boolean indicating whether selecting multiple files for upload is allowed.
+
+### Customizing for a Specific Form
+
+It is also possible to customize the markup for a specific Form.
+
+You will need to create folder using the ID of the Form: `~\Views\Partials\Forms\{FormId}` (find the id of the Form in the URL when you are viewing the Form in the backoffice.)
+
+
+
+As an example if your Form ID is 0d3e6b2d-db8a-43e5-8f28-36241d712487 then you can overwrite the Form view by adding the `Form.cshtml` file to the directory. Start by copying the default one and then making your custom changes: `~\Views\Partials\Forms\0d3e6b2d-db8a-43e5-8f28-36241d712487\Form.cshtml`.
+
+You can also overwrite views for one or more fieldtypes by adding the views to the `Fieldtypes` folder: `~\Views\Partials\Forms\0d3e6b2d-db8a-43e5-8f28-36241d712487\Fieldtypes\Fieldtype.Textfield.cshtml`.
diff --git a/16/umbraco-forms/developer/email-templates.md b/16/umbraco-forms/developer/email-templates.md
new file mode 100644
index 00000000000..6e175528457
--- /dev/null
+++ b/16/umbraco-forms/developer/email-templates.md
@@ -0,0 +1,245 @@
+---
+description: "Creating an email template for Umbraco Forms."
+---
+
+# Email Templates
+
+We include a Workflow **Send email with template (Razor)** that allows you to pick a Razor view file that can be used to send out a _pretty HTML email_ for Form submissions.
+
+## Creating an Email Template
+
+If you wish to have one or more templates to choose from the **Send email with template (Razor)**, you will need to place all email templates into the `~/Views/Partials/Forms/Emails/` folder.
+
+The Razor view must inherit from FormsHtmlModel:
+
+```csharp
+@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage
+```
+
+You now have a model that contains your Form fields which can be used in your email HTML markup, along with the UmbracoHelper methods such as `Umbraco.TypedContent` and `Umbraco.TypedMedia` etc.
+
+Below is an example of an email template from the `~/Views/Partials/Forms/Emails/` folder:
+
+```csharp
+@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage
+
+@{
+ //This is an example email template where you can use Razor Views to send HTML emails
+
+ //You can use Umbraco.TypedContent & Umbraco.TypedMedia etc to use Images & content from your site
+ //directly in your email templates too
+
+ //Strongly Typed
+ //@Model.GetValue("aliasFormField")
+ //@foreach (var color in Model.GetValues("checkboxField")){}
+
+ //Dynamics
+ //@Model.DynamicFields.aliasFormField
+ //@foreach(var color in Model.DynamicFields.checkboxField
+
+ //Images need to be absolute - so fetching domain to prefix with images
+ var siteDomain = Context.Request.Scheme + "://" + Context.Request.Host;
+ var assetUrl = siteDomain + "/App_plugins/UmbracoForms/Assets/Email-Example";
+
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Umbraco Forms
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This is an example email template from Umbraco Forms Razor based email templates. You can build forms using any HTML markup you wish.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Form Results
+
+
+
+
+
+
+
+ @foreach (var field in Model.Fields)
+ {
+ @field.Name
+
+ switch (field.FieldType)
+ {
+ case "FieldType.FileUpload.cshtml":
+ @field.GetValue()
+ break;
+
+ case "FieldType.DatePicker.cshtml":
+ DateTime dt;
+ var fieldValue = field.GetValue();
+ var dateValid = DateTime.TryParse(fieldValue != null ? fieldValue.ToString() : string.Empty, out dt);
+ var dateStr = dateValid ? dt.ToString("f") : "";
+ @dateStr
+ break;
+
+ case "FieldType.CheckBoxList.cshtml":
+
+ @foreach (var color in field.GetValues())
+ {
+ @color
+ }
+
+ break;
+ default:
+ @field.GetValue()
+ break;
+ }
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
diff --git a/16/umbraco-forms/developer/extending/README.md b/16/umbraco-forms/developer/extending/README.md
new file mode 100644
index 00000000000..635b9fd8f8e
--- /dev/null
+++ b/16/umbraco-forms/developer/extending/README.md
@@ -0,0 +1,83 @@
+# Extending
+
+Umbraco Forms functionality can be extended in different ways. In this section we focus on techniques available to a back-end/C# developer.
+
+For front-end extensions, specifically via theming, see the [Themes](../themes.md) section.
+
+## Developing Custom Providers
+
+Although the Forms package comes with many fields, workflows and other built-in types, you can still create and develop your own if needed.
+
+### [Provider model](adding-a-type.md)
+
+Many features of Forms use a provider model, which makes it quicker to add new parts to the application.
+
+The model uses the notion that everything must have a type to exist. The type defines the capabilities of the item. For instance a Textfield on a form has a FieldType, this particular field type enables it to render an input field and save text strings. The same goes for workflows, which have a workflow type, datasources which have a datasource type and so on. Using the model you can seamlessly add new types and thereby extend the application.
+
+It is possible to add new Field types, Data Source Types, Prevalue Source Types, Export Types, and Workflow Types.
+
+### [Field types](adding-a-fieldtype.md)
+
+A field type handles rendering of the UI for a field in a form. It renders a standard ASP.NET Razor partial view and is able to return a list of values when the form is saved.
+
+The concept of provider settings, common to the field and other types, is also discussed in this section.
+
+### Data Source Types
+
+A data source type enables Umbraco Forms to connect to a custom source of data. A data source consists of any kind of storage if it is possible to return a list of fields Umbraco Forms can map values to. For example: a Database data source can return a list of columns Forms can send data to. This enables Umbraco Forms to map a form to a data source. A data source type is responsible for connecting Forms to external storage.
+
+### [Prevalue Source Types](adding-a-prevaluesourcetype.md)
+
+A prevalue source type connects to 3rd party storage to retrieve values. These values are used on fields supporting prevalues. The source fetches the collection of values.
+
+### [Workflow Types](adding-a-workflowtype.md)
+
+A workflow can be executed each time a form changes state (when it is submitted for instance). A workflow is responsible for executing logic which can modify the record or notify 3rd party systems.
+
+### [Export Types](adding-a-exporttype.md)
+
+Export types are responsible for turning form records into any other data format, which is then returned as a file.
+
+### [Magic String Format Functions](adding-a-magic-string-format-function.md)
+
+Custom magic string format functions to add to the [ones shipped with Umbraco Forms](../magic-strings.md#formatting-magic-strings) can be created in code.
+
+### [Validation Patterns](adding-a-validation-pattern.md)
+
+When creating a text field in Umbraco Forms, a validation pattern in the form of a regular expression can be applied. Default patterns can be removed or re-ordered, and custom ones created and added.
+
+## Handling Forms Events
+
+Another option for extension via custom code is to hook into one of the many events available.
+
+### [Validation](adding-an-event-handler.md)
+
+Form events are raised during the submission life cycle and can be handled for executing custom logic.
+
+### [Default Fields and Workflows](customize-default-workflows.md)
+
+When a new form is created, the default behavior is to add a single workflow. This workflow will send a copy of the form to the current backoffice user's email address.
+
+A single "data consent" field will also be added unless it has been disabled via configuration.
+
+It's possible to amend this behavior and change it to fit your needs.
+
+## Responding to State Values
+
+In the course of submitting a form, Umbraco Forms will set values in `TempData` and/or `HttpContext.Items`, that you can use to customize the website functionality.
+
+### Customizing Post-Submission Behavior
+
+Whether displaying a message or redirecting, a developer can customize the page viewed after the form is submitted based on the presence of `TempData` variables.
+
+One variable with a key of `UmbracoFormSubmitted` has a value containing the Guid identifier for the submitted form.
+
+A second variable contains the Guid identifier of the record created from the form submission. You can find this using the `Forms_Current_Record_id` key.
+
+In order to redirect to an external URL rather than a selected page on the Umbraco website, you will need to use a [custom workflow](adding-a-workflowtype.md). Within this workflow you can set the required redirect URL on the `HttpContext.Items` dictionary using the key `FormsRedirectAfterFormSubmitUrl` (defined in the constant `Umbraco.Forms.Core.Constants.ItemKeys.RedirectAfterFormSubmitUrl`).
+
+For example, using an injected instance of `IHttpContextAccessor`:
+
+```
+_httpContextAccessor.HttpContext.Items[Constants.ItemKeys.RedirectAfterFormSubmitUrl] = "https://www.umbraco.com";
+```
diff --git a/16/umbraco-forms/developer/extending/adding-a-exporttype.md b/16/umbraco-forms/developer/extending/adding-a-exporttype.md
new file mode 100644
index 00000000000..22495e7db8b
--- /dev/null
+++ b/16/umbraco-forms/developer/extending/adding-a-exporttype.md
@@ -0,0 +1,243 @@
+# Adding An Export Type To Umbraco Forms
+
+_This builds on the "_[_adding a type to the provider model_](adding-a-type.md)_" chapter._
+
+Add a new class to your project and have it inherit from `Umbraco.Forms.Core.ExportType`. You have two options when implementing the class, as shown in the following examples.
+
+## Basic Example
+
+You can implement the method `public override string ExportRecords(RecordExportFilter filter)` in your export provider class. You need to return a string you wish to write to a file. For example, you can generate a `.csv` (comma-separated values) file. You would perform your logic to build up a comma-separated string in the `ExportRecords` method.
+
+{% hint style="info" %}
+In the constructor of your provider, you will need a further two properties, `FileExtension` and `Icon`.
+{% endhint %}
+
+`FileExtension` is the extension such as `zip`, `txt` or `csv` of the file you will be generating and serving from the file system.
+
+In this example below we will create a single HTML file which takes all the submissions/entries to be displayed as a HTML report. We will do this in conjunction with a Razor partial view to help build up our HTML and thus merge it with the form submission data to generate a string of HTML.
+
+### Provider Class
+
+```csharp
+using System;
+using Umbraco.Cms.Core.Hosting;
+using Umbraco.Forms.Core;
+using Umbraco.Forms.Core.Models;
+using Umbraco.Forms.Core.Searchers;
+using Umbraco.Forms.Web.Helpers;
+
+namespace MyFormsExtensions
+{
+ public class ExportToHtml : ExportType
+ {
+ private readonly IHttpContextAccessor _httpContextAccessor;
+ private readonly IFormRecordSearcher _formRecordSearcher;
+
+ public ExportToHtml(
+ IHostEnvironment hostEnvironment,
+ IHttpContextAccessor httpContextAccessor,
+ IFormRecordSearcher formRecordSearcher)
+ : base(hostEnvironment)
+ {
+ _httpContextAccessor = httpContextAccessor;
+ _formRecordSearcher = formRecordSearcher;
+
+ Name = "Export as HTML";
+ Description = "Export entries as a single HTML report";
+ Id = new Guid("4117D352-FB41-4A4C-96F5-F6EF35B384D2");
+ FileExtension = "html";
+ Icon = "icon-article";
+ }
+
+ public override string ExportRecords(RecordExportFilter filter)
+ {
+ var view = "~/Views/Partials/Forms/Export/html-report.cshtml";
+ EntrySearchResultCollection model = _formRecordSearcher.QueryDataBase(filter);
+ return ViewHelper.RenderPartialViewToString(_httpContextAccessor.GetRequiredHttpContext(), view, model);
+ }
+ }
+}
+```
+
+### Razor Partial View
+
+```csharp
+@model Umbraco.Forms.Core.Searchers.EntrySearchResultCollection
+
+@{
+ var submissions = Model.Results.ToList();
+ var schemaItems = Model.Schema.ToList();
+}
+
+Form Submissions
+
+@foreach (var submission in submissions)
+{
+ var values = submission.Fields.ToList();
+
+ for (int i = 0; i < schemaItems.Count; i++)
+ {
+ @schemaItems[i].Name @values[i].Value
+
+ }
+
+
+}
+```
+
+### Registration
+
+```csharp
+using Umbraco.Cms.Core.Composing;
+using Umbraco.Forms.Core.Providers.Extensions;
+using Umbraco.Forms.TestSite.Business.ExportTypes;
+
+namespace MyFormsExtensions
+{
+ public class TestComposer : IComposer
+ {
+ public void Compose(IUmbracoBuilder builder)
+ {
+ builder.FormsExporters().Add();
+ }
+ }
+}
+```
+
+## Advanced Example
+
+This approach gives us more flexibility in creating the file we wish to serve as the exported file. We do this for the export to Excel file export provider we ship in Umbraco Forms. With this we can use a library to create the Excel file and store it in a temporary location before we send back the filepath for the browser to stream down the export file.
+
+In this example we will create a collection of text files, one for each submission which is then zipped up into a single file and served as the export file.
+
+```csharp
+using System;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using Umbraco.Cms.Core.Hosting;
+using Umbraco.Forms.Core;
+using Umbraco.Forms.Core.Models;
+using Umbraco.Forms.Core.Searchers;
+
+namespace MyFormsExtensions
+{
+ public class ExportToTextFiles : ExportType
+ {
+ private readonly IFormRecordSearcher _formRecordSearcher;
+
+ public ExportToTextFiles(
+ IHostingEnvironment hostingEnvironment,
+ IFormRecordSearcher formRecordSearcher)
+ : base(hostingEnvironment)
+ {
+ _formRecordSearcher = formRecordSearcher;
+
+ this.Name = "Export as text files";
+ this.Description = "Export entries as text files inside a zip file";
+ this.Id = new Guid("171CABC9-2207-4575-83D5-2A77E824D5DB");
+ this.FileExtension = "zip";
+ this.Icon = "icon-zip";
+ }
+
+ ///
+ /// We do not implement this method from the interface
+ /// As this method is called from ExportToFile that we also override here & is expecting the file contents as a string to be written as a stream to a file
+ /// Which would be OK if we were creating a CSV or a single based file that can have a simple string written as a string such as one large HTML report or XML file perhaps
+ ///
+ public override string ExportRecords(RecordExportFilter filter) => throw new NotImplementedException();
+
+ ///
+ /// This gives us greater control of the export process
+ ///
+ ///
+ /// This filter contains the date range & other search parameters to limit the entries we are exporting
+ ///
+ ///
+ /// The filepath that the export file is expecting to be served from
+ /// So ensure that the zip of text files is saved at this location
+ ///
+ /// The final file path to serve up as the export - this is unlikely to change through the export logic
+ public override string ExportToFile(RecordExportFilter filter, string filepath)
+ {
+ // Before Save - Check Path, Directory & Previous File export does not exist
+ string pathToSaveZipFile = filepath;
+
+ // Check our path does not contain \\
+ // If not, use the filePath
+ if (filepath.Contains('\\') == false)
+ {
+ pathToSaveZipFile = HostingEnvironment.MapPathContentRoot(filepath);
+ }
+
+ // Get the directory (strip out \\ if it exists)
+ var dir = filepath.Substring(0, filepath.LastIndexOf('\\'));
+ var tempFileDir = Path.Combine(dir, "text-files");
+
+
+ // If the path does not end with our file extension, ensure it's added
+ if (pathToSaveZipFile.EndsWith("." + FileExtension) == false)
+ {
+ pathToSaveZipFile += "." + FileExtension;
+ }
+
+ // Check that the directory where we will save the ZIP file temporarily exists
+ // If not just create it
+ if (Directory.Exists(tempFileDir) == false)
+ {
+ Directory.CreateDirectory(tempFileDir);
+ }
+
+ // Check if the zip file exists already - if so delete it, as we have a new update
+ if (File.Exists(pathToSaveZipFile))
+ {
+ File.Delete(pathToSaveZipFile);
+ }
+
+ // Query the DB for submissions to export based on the filter
+ EntrySearchResultCollection submissions = _formRecordSearcher.QueryDataBase(filter);
+
+ // Get the schema objects to a list so we can get items using position index
+ var schemaItems = submissions.schema.ToList();
+
+ // We will use this to store our contents of our file to save as a text file
+ var fileContents = string.Empty;
+
+ // For each submission we have build up a string to save to a text file
+ foreach (EntrySearchResult submission in submissions.Results)
+ {
+ // The submitted data for the form submission
+ var submissionData = submission.Fields.ToList();
+
+ // For loop to match the schema position to the submission data
+ for (int i = 0; i < schemaItems.Count; i++)
+ {
+ // Concat a string of the name of the field & its stored data
+ fileContents += schemaItems[i].Name + ": " + submissionData[i] + Environment.NewLine;
+ }
+
+ // Now save the contents to a text file
+ // Base it on the format of the record submission unique id
+ var textFileName = Path.Combine(tempFileDir, submission.UniqueId + ".txt");
+ File.WriteAllText(textFileName, fileContents);
+
+ // Reset fileContents to be empty again
+ fileContents = string.Empty;
+ }
+
+ // Now we have a temp folder full of text files
+ // Generate a zip file containing them & save that
+ ZipFile.CreateFromDirectory(tempFileDir, pathToSaveZipFile);
+
+ // Tidy up after ourselves & delete the temp folder of text files
+ if (Directory.Exists(tempFileDir))
+ {
+ Directory.Delete(tempFileDir, true);
+ }
+
+ // Return the path where we saved the zip file containing the text files
+ return pathToSaveZipFile;
+ }
+ }
+}
+```
diff --git a/16/umbraco-forms/developer/extending/adding-a-fieldtype.md b/16/umbraco-forms/developer/extending/adding-a-fieldtype.md
new file mode 100644
index 00000000000..8ab71b312c6
--- /dev/null
+++ b/16/umbraco-forms/developer/extending/adding-a-fieldtype.md
@@ -0,0 +1,515 @@
+# Adding A Field Type To Umbraco Forms
+
+_This builds on the "_[_adding a type to the provider model_](adding-a-type.md)_" chapter_
+
+In this article, we will illustrate how to add a custom form field type using server-side and client-side components. We will use the example of rendering a "slider" field type that allows the user to select a number within a specific range of values.
+
+## Server-side Field Type Definition
+
+Add a new class to the Visual Studio solution. Inherit from `Umbraco.Forms.Core.FieldType` and complete as follows:
+
+```csharp
+using Umbraco.Cms.Core.Composing;
+using Umbraco.Forms.Core.Attributes;
+using Umbraco.Forms.Core.Enums;
+using Umbraco.Forms.Core.Providers;
+
+namespace MyProject;
+
+public class SliderFieldType : Core.FieldType
+{
+ public SliderFieldType()
+ {
+ Id = new Guid("6dff0075-598c-4345-89d7-e0db8684c819");
+ Name = "Slider";
+ Alias = "slider";
+ Description = "Render a UUI Slider field.";
+ Icon = "icon-autofill";
+ DataType = FieldDataType.String;
+ SortOrder = 10;
+
+ FieldTypeViewName = "FieldType.Slider.cshtml";
+ EditView = "My.PropertyEditorUi.InputNumber";
+ PreviewView = "My.FieldPreview.Slider";
+ }
+
+ [Setting("Minimum", Description = "Minimum value", View = "Umb.PropertyEditorUi.Integer", DisplayOrder = 10)]
+ public virtual string? Min { get; set; } = "1";
+
+ [Setting("Maximum", Description = "Maximum value", View = "Umb.PropertyEditorUi.Integer", DisplayOrder = 20)]
+ public virtual string? Max { get; set; } = "1";
+
+ [Setting("Step", Description = "Step size", View = "Umb.PropertyEditorUi.Integer", DisplayOrder = 30)]
+ public virtual string? Step { get; set; } = "1";
+
+ [Setting("Default Value", Description = "Default value", View = "Umb.PropertyEditorUi.Integer", DisplayOrder = 40)]
+ public virtual string? DefaultValue { get; set; } = "1";
+
+ [Setting("Hide step values", Description = "Hides the numbers representing the value of each steps. Dots will still be visible", View = "Umb.PropertyEditorUi.Toggle", DisplayOrder = 50)]
+ public virtual string? HideStepValues { get; set; }
+
+ [Setting("Background color", Description = "Background color for the input field", View = "My.PropertyEditorUi.InputColor", DisplayOrder = 60)]
+ public virtual string? BgColor { get; set; } = "1";
+}
+```
+
+In the constructor or via overridden properties, we can specify details of the field type:
+
+* `Id` - should be set to a unique GUID.
+* `Alias` - an internal alias for the field, used for localized translation keys.
+* `Name` - the name of the field presented in the backoffice.
+* `Description` - the description of the field presented in the backoffice.
+* `Icon` - the icon of the field presented in the backoffice form builder user interface.
+* `DataType` - specifies the type of data stored by the field. Options are `String`, `LongString`, `Integer`, `DataTime` or `Bit` (boolean).
+* `SupportsMandatory` - indicates whether mandatory validation can be used with the field (defaults to `true`).
+* `MandatoryByDefault` - indicates whether the field will be mandatory by default when added to a form (defaults to `false`).
+* `SupportsRegex` - indicates whether pattern-based validation using regular expressions can be used with the field (defaults to `false`).
+* `SupportsPreValues` - indicates whether prevalues are supported by the field (defaults to `false`).
+* `RenderInputType`- indicates how the field should be rendered within the theme as defined with the `RenderInputType` enum.
+ * The default is `Single` for a single input field.
+ * `Multiple` should be used for multiple input fields such as checkbox lists.
+ * `Custom` is used for fields without visible input fields.
+* `FieldTypeViewName` - indicates the name of the partial view used to render the field on the website.
+* `EditView` - indicates the name of a property editor UI that is used for editing the field in the backoffice. If nothing is provided, the built-in label will be used and the field won't be editable.
+* `PreviewView` - indicates the name of a manifest registered client-side resource that is used for previewing the field in the backoffice. If nothing is provided, the name of the field type will be used as the preview.
+
+You now need to register this new field as a dependency:
+
+```csharp
+using Umbraco.Cms.Core.Composing;
+using Umbraco.Cms.Core.DependencyInjection;
+using Umbraco.Forms.Core.Providers;
+
+namespace MyProject;
+
+public class Startup : IComposer
+{
+ public void Compose(IUmbracoBuilder builder)
+ {
+ builder.WithCollectionBuilder()
+ .Add();
+ }
+}
+```
+
+## Partial View
+
+We will start building the view for the default theme of the Form at `Views\Partials\Forms\Themes\default\FieldTypes\FieldType.Slider.cshtml`.
+
+The file name for the partial view should match the value set on the `FieldTypeViewName` property.
+
+```csharp
+@using Umbraco.Forms.Web
+@model Umbraco.Forms.Web.Models.FieldViewModel
+@{
+ var min = Model.GetSettingValue("Min", 1);
+ var max = Model.GetSettingValue("Max", 10);
+ var step = Model.GetSettingValue("Step", 1);
+ var bgColor = Model.GetSettingValue("BgColor", "#fff");
+}
+This is a custom "slider" field type. We'll just use an input to mock this up.
+
+```
+
+This will be rendered when the default theme is used.
+
+If working with Umbraco 9 or earlier versions, you'll find the `Views\Partials\Forms\Themes\default\` folder on disk and can create the files there.
+
+For Umbraco 10 and above, we've moved to [distributing the theme as part of a Razor Class Library](../../upgrading/version-specific/#views-and-client-side-files) so the folder won't exist. However, you can create it for your custom field type. If you would like to reference the partial views of the default theme, you can download them as mentioned in the [Themes](../themes.md) article.
+
+### Read-only partial view
+
+When rendering a multi-page form, editors have the option to display a summary page where the entries can be viewed before submitting.
+
+To support this, a read-only view of the field is necessary.
+
+For most fields, nothing is required here, as the default read-only display defined in the built-in `ReadOnly.cshtml` file suffices.
+
+However, if you want to provide a custom read-only display for your field, you can do so by creating a second partial view. This should be named with a `.ReadOnly` suffix. For this example, you would create `FieldType.Slider.ReadOnly.cshtml`.
+
+## Field Settings
+
+Field settings will be managed in the backoffice by editors who will create forms using the custom field type. These settings can be added to the C# class as properties with a `Setting` attribute:
+
+```csharp
+[Setting("Minimum", Description = "Minimum value", View = "Umb.PropertyEditorUi.Integer", DisplayOrder = 10)]
+public virtual string? Min { get; set; } = "1";
+```
+
+The property `Name` names the setting in the backoffice with the `Description` providing the help text. Both of these can be translated, as discussed in the backoffice components section below.
+
+The `View` property indicates a property editor UI used for editing the setting value. You can use a built-in property editor UI, one from a package, or a custom one registered with your solution. The default value if not provided is `Umb.PropertyEditorUi.TextBox`, which will use the standard Umbraco text box property editor UI.
+
+`SupportsPlaceholders` is a flag indicating whether the setting can contain ["magic string" placeholders](../magic-strings.md) and controls whether they are parsed on rendering.
+
+`HtmlEncodeReplacedPlaceholderValues` takes effect only if `SupportsPlaceholders` is `true`. It controls whether the replaced placeholder values should be HTML encoded (as is necessary for rendering within content from a rich text editor).
+
+`SupportsHtml` is a flag indicating whether the setting can contain HTML content. When set to `true` it will be treated as HTML content when the value is read from the Forms delivery API.
+
+`IsMandatory` if set to `true` will provide client-side validation in the backoffice to ensure the value is completed.
+
+When creating a field or other provider type, you might choose to inherit from an existing class. This could be if one of the types provided with Umbraco Forms almost meets your needs but you want to make some changes.
+
+All setting properties for the Forms provider types are marked as `virtual`, so you can override them and change the setting values:
+
+## Umbraco Backoffice Components
+
+With Forms 14+, aspects of the presentation and functionality of the custom field are handled by client-side components, registered via manifests:
+
+* The preview, displayed on the form definition editor.
+* The property editor UI used for editing the the submitted values via the backoffice.
+* The property editor UI used for editing settings.
+* A settings converter, that handles configuring the property editor and translating between the editor and persisted values.
+* Translations for setting labels and descriptions.
+
+To create custom backoffice components for Umbraco 14, it's recommended to use a front-end build setup using Vite, TypeScript, and Lit. For more information, see the [Extension with Vite, TypeScript, and Lit](https://app.gitbook.com/s/G1Byxw7XfiZAj8zDMCTD/tutorials/creating-your-first-extension#extension-with-vite-typescript-and-lit) article.
+
+To display a name and description on a custom field, you need to register a JavaScript file as shown in the [Localization](https://app.gitbook.com/s/7MBVdnTbFiAgWuRsHpNS/customizing/extending-overview/extension-types/localization) article.
+
+### Field Preview
+
+The alias of the preview to use is defined on the field type via the `PreviewView` property.
+
+A preview for our slider, representing the selected setting values could look as follows:
+
+```javascript
+import { UmbElementMixin } from "@umbraco-cms/backoffice/element-api";
+import {
+ LitElement,
+ css,
+ customElement,
+ html,
+ property,
+} from "@umbraco-cms/backoffice/external/lit";
+
+const elementName = "my-field-preview-slider";
+
+@customElement(elementName)
+export class MyFieldPreviewSliderElement extends UmbElementMixin(LitElement) {
+ @property()
+ settings = {};
+
+ @property({ type: Array })
+ prevalues = [];
+
+ getSettingValue(key: string) {
+ return this.settings[key];
+ }
+
+ render() {
+ return html`
+
+
`;
+ }
+
+ static styles = css`
+ div {
+ padding: var(--uui-size-4);
+ }
+ `;
+}
+
+export default MyFieldPreviewSliderElement;
+
+declare global {
+ interface HTMLElementTagNameMap {
+ [elementName]: MyFieldPreviewSliderElement;
+ }
+}
+```
+
+And it is registered via a manifest:
+
+```javascript
+import MyFieldPreviewSliderElement from './slider-preview.element.js';
+
+const sliderPreviewManifest = {
+ type: "formsFieldPreview",
+ alias: "My.FieldPreview.Slider",
+ name: "Forms UUI Slider Field Preview",
+ api: MyFieldPreviewSliderElement,
+ element: () => import('./slider-preview.element.js')
+ };
+
+ export const manifests = [sliderPreviewManifest];
+```
+
+### Field Editor
+
+Umbraco Forms supports editing of the entries submitted by website visitors via the backoffice. The property editor interface to use for this is defined in the field type's `EditView` property.
+
+If not using a built-in property editor, you can create your own. The following example shows how the numerical entries could be edited using an input control.
+
+```javascript
+import {
+ html,
+ customElement,
+} from "@umbraco-cms/backoffice/external/lit";
+import type { UmbPropertyEditorUiElement } from "@umbraco-cms/backoffice/property-editor";
+import { UmbLitElement } from "@umbraco-cms/backoffice/lit-element";
+import {
+ UmbChangeEvent,
+} from "@umbraco-cms/backoffice/event";
+import { UmbFormControlMixin } from "@umbraco-cms/backoffice/validation";
+
+const elementName = "my-property-editor-ui-number";
+
+@customElement(elementName)
+export class MyPropertyEditorUINumberElement
+ extends UmbFormControlMixin(UmbLitElement, undefined)
+ implements UmbPropertyEditorUiElement
+{
+ private onChange(e: Event) {
+ const newValue = (e.target as HTMLInputElement).value;
+ if (newValue === this.value) return;
+ this.value = newValue;
+ this.dispatchEvent(new UmbChangeEvent());
+ }
+
+ override render() {
+ return html` `;
+ }
+}
+
+export default MyPropertyEditorUINumberElement;
+
+declare global {
+ interface HTMLElementTagNameMap {
+ [elementName]: MyPropertyEditorUINumberElement;
+ }
+}
+```
+
+Again, it's registered via a manifest.
+
+```javascript
+const numberPropertyEditorManifest = {
+ type: 'propertyEditorUi',
+ alias: 'My.PropertyEditorUi.InputNumber',
+ name: 'Number Input Property Editor UI',
+ element: () => import('./property-editor-ui-number.element.js'),
+ meta: {
+ label: 'Number Input',
+ icon: 'icon-autofill',
+ },
+};
+export const manifests = [numberPropertyEditorManifest];
+```
+
+### Setting Value Editor
+
+Field type settings also use a property editor UI for editing the values in the backoffice. The one to use is defined via the `View` property on the `Setting` attribute.
+
+In our example we use a custom one, allowing the value for the background color to the field to be selected via an input control.
+
+```javascript
+import {
+ html,
+ customElement,
+ type PropertyValueMap,
+} from "@umbraco-cms/backoffice/external/lit";
+import type { UmbPropertyEditorUiElement } from "@umbraco-cms/backoffice/property-editor";
+import { UmbLitElement } from "@umbraco-cms/backoffice/lit-element";
+import {
+ UmbChangeEvent,
+} from "@umbraco-cms/backoffice/event";
+import { UmbFormControlMixin } from "@umbraco-cms/backoffice/validation";
+
+const elementName = "my-property-editor-ui-color";
+
+@customElement(elementName)
+export class MyPropertyEditorUIColorElement
+ extends UmbFormControlMixin(UmbLitElement, undefined)
+ implements UmbPropertyEditorUiElement
+{
+ protected firstUpdated(
+ _changedProperties: PropertyValueMap | Map
+ ): void {
+ super.firstUpdated(_changedProperties);
+ this.addFormControlElement(this.shadowRoot!.querySelector("input")!);
+ }
+
+ private onChange(e: Event) {
+ const newValue = (e.target as HTMLInputElement).value;
+ if (newValue === this.value) return;
+ this.value = newValue;
+ this.dispatchEvent(new UmbChangeEvent());
+ }
+
+ override render() {
+ return html` `;
+ }
+}
+
+export default MyPropertyEditorUIColorElement;
+
+declare global {
+ interface HTMLElementTagNameMap {
+ [elementName]: MyPropertyEditorUIColorElement;
+ }
+}
+```
+
+And register it via a manifest:
+
+```javascript
+const colorPropertyEditorManifest = {
+ type: 'propertyEditorUi',
+ alias: 'My.PropertyEditorUi.InputColor',
+ name: 'Color Input Property Editor UI',
+ element: () => import('./property-editor-ui-color.element.js'),
+ meta: {
+ label: 'Color Input',
+ icon: 'icon-autofill',
+ },
+};
+
+export const manifests = [colorPropertyEditorManifest];
+```
+
+### Setting Value Converter
+
+You may want to consider registering a settings value converter. This is another client-side component that is registered in a manifest. It converts between the setting value required for the editor and the value persisted with the form definition. A converter defines three methods:
+
+* `getSettingValueForEditor` - converts the persisted string value into one suitable for the editor
+* `getSettingValueForPersistence` - converts the editor value into the string needed for persistence
+* `getSettingPropertyConfig` - creates the configuration needed for the property editor
+
+The following code shows the structure for these converter elements.
+
+```javascript
+import type { UmbPropertyValueData } from "@umbraco-cms/backoffice/property";
+
+export class SliderSettingValueConverter {
+
+ async getSettingValueForEditor(setting, alias: string, value: string) {
+ return Promise.resolve(value);
+ }
+
+ async getSettingValueForPersistence(setting, valueData: UmbPropertyValueData) {
+ return Promise.resolve(valueData.value);
+ }
+
+ async getSettingPropertyConfig(setting, alias: string, values: UmbPropertyValueData[]) {
+ return Promise.resolve([]);
+ }
+}
+```
+
+It's registered as follows. The `propertyEditorUiAlias` matches with the property editor UI that requires the conversions.
+
+```javascript
+import { SliderSettingValueConverter } from "./slider-setting-value-converter.api";
+
+const sliderValueConverterManifest = {
+ type: "formsSettingValueConverter",
+ alias: "My.SettingValueConverter.Slider",
+ name: "Slider Value Converter",
+ propertyEditorUiAlias: "My.PropertyEditorUi.Slider",
+ api: SliderSettingValueConverter,
+};
+
+export const manifests = [sliderValueConverterManifest];
+```
+
+### Language Files
+
+Setting labels and descriptions can be translated via language files. If no client-side localization is provided, the values provided server-side in the `Setting` attribute's `Name` and `Description` properties will be used.
+
+The following example shows how this is created for the settings on our example field type:
+
+```javascript
+import type { UmbLocalizationDictionary } from "@umbraco-cms/backoffice/localization-api";
+
+export default {
+ formProviderFieldTypes: {
+ sliderMinLabel: `Minimum`,
+ sliderMinDescription: `Minimum value`,
+ sliderMaxLabel: `Maximum`,
+ sliderMaxDescription: `Maximum value`,
+ sliderStepLabel: `Step`,
+ sliderStepDescription: `Step size`,
+ sliderDefaultValueLabel: `Default Value`,
+ sliderDefaultValueDescription: `Default value shown when the slider is displayed`,
+ sliderHideStepValuesLabel: `Hide step values`,
+ sliderHideStepValuesDescription: `Indicate whether the the field's label should be shown when rendering the form`,
+ sliderBgColorLabel: `Background color`,
+ sliderBgColorDescription: `Background color for the field`,
+ },
+}
+```
+
+Each different type of extension for Forms uses a different root value:
+
+* Data sources - `formProviderDataSources`
+* Export types - `formProviderExportTypes`
+* Field types - `formProviderFieldTypes`
+* Prevalue sources - `formProviderPrevalueSources`
+* Recordset actions - `formRecordSetActions`
+* Workflows - `formProviderWorkflows`
+
+The language files are registered with:
+
+```javascript
+import type { ManifestLocalization } from '@umbraco-cms/backoffice/localization';
+
+const localizationManifests: Array = [
+ {
+ type: "localization",
+ alias: "My.Localization.En_US",
+ weight: -100,
+ name: "English (US)",
+ meta: {
+ culture: "en-us",
+ },
+ js: () => import("./en-us.js"),
+ },
+];
+export const manifests = [...localizationManifests];
+```
+
+### Registering the Components
+
+Finally, you will need an entry point to your client-side components that will register the manifests with Umbraco's extension registry. For example:
+
+```javascript
+import { manifests as propertyEditorManifests } from "./property-editor/manifests.js";
+import { manifests as fieldPreviewManifests } from "./field-preview/manifests.js";
+import { manifests as settingValueConverterManifests } from "./setting-value-converter/manifests.js";
+import { manifests as localizationManifests } from "./lang/manifests.js";
+
+const manifests = [
+ ...propertyEditorManifests,
+ ...fieldPreviewManifests,
+ ...settingValueConverterManifests,
+ ...localizationManifests
+];
+
+export const onInit = async (host, extensionRegistry) => {
+ extensionRegistry.registerMany(manifests);
+};
+```
diff --git a/16/umbraco-forms/developer/extending/adding-a-magic-string-format-function.md b/16/umbraco-forms/developer/extending/adding-a-magic-string-format-function.md
new file mode 100644
index 00000000000..e7fcd23d89a
--- /dev/null
+++ b/16/umbraco-forms/developer/extending/adding-a-magic-string-format-function.md
@@ -0,0 +1,86 @@
+# Adding a Magic String Format Function
+
+_This builds on the "_[_adding a type to the provider model_](adding-a-type.md)_" chapter_
+
+Umbraco Forms [Magic Strings](../magic-strings.md) can be used to replace placeholders within form elements with values from different sources. Sources include the HTTP request or the Umbraco page where the form is hosted.
+
+These values can be formatted using [filter functions](../magic-strings.md#formatting-magic-strings).
+
+Filter functions for common operations such as truncating a string or formatting a date or number are provided. It's also possible to create custom ones in code.
+
+## Creating a custom format function
+
+To create a custom format function, create a class that implements `IParsedPlaceholderFormatter`.
+
+The `FunctionName` property provides the name of the function that will be used within the form's magic string.
+
+The `FormatValue` property parses the provided value and arguments and returns the formatted value as a string.
+
+The following example shows the implementation of a function that bounds an integer value. It takes two arguments, a minimum and maximum value. If the value read from the magic string source is numeric, and fits within the two bounds, it is returned. Otherwise, either the minimum or maximum value is returned depending on whether the value is lower or higher than the bounds respectively.
+
+```csharp
+using System.Globalization;
+using Umbraco.Forms.Core.Interfaces;
+
+namespace Umbraco.Forms.Core.Providers.ParsedPlacholderFormatters
+{
+ public class BoundNumber : IParsedPlaceholderFormatter
+ {
+ public string FunctionName => "bound";
+
+ public string FormatValue(string value, string[] args)
+ {
+ if (args.Length != 2)
+ {
+ return value;
+ }
+
+ if (!int.TryParse(args[0], out var min) || !int.TryParse(args[1], out var max))
+ {
+ return value;
+ }
+
+ if (int.TryParse(value, out int valueAsInteger) ||
+ int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out valueAsInteger))
+ {
+ if (valueAsInteger < min)
+ {
+ return min.ToString();
+ }
+
+ if (valueAsInteger > max)
+ {
+ return max.ToString();
+ }
+
+ return valueAsInteger.ToString();
+ }
+
+ return value;
+ }
+ }
+}
+```
+
+## Registering the custom format function
+
+As with other provider types, the custom function needs to be registered. An example registration using the `IUmbracoBuilder` is shown below:
+
+```csharp
+public static IUmbracoBuilder AddCustomProviders(this IUmbracoBuilder builder)
+{
+ builder.FormsParsedPlaceholderFormatters()
+ .Add();
+ return builder;
+}
+```
+
+## Using the custom format function
+
+The format function can be used within a form's magic string in the same way as the ones provided with Umbraco Forms.
+
+For the example provided, it would be used like this:
+
+```
+[#field | bound: 1: 10]
+```
diff --git a/16/umbraco-forms/developer/extending/adding-a-prevaluesourcetype.md b/16/umbraco-forms/developer/extending/adding-a-prevaluesourcetype.md
new file mode 100644
index 00000000000..e7535c9526d
--- /dev/null
+++ b/16/umbraco-forms/developer/extending/adding-a-prevaluesourcetype.md
@@ -0,0 +1,219 @@
+# Adding A Prevalue Source Type To Umbraco Forms
+
+_This builds on the "_[_Adding a type to the provider model_](adding-a-type.md)_" article_
+
+Add a new class to your project - inherit it from `Umbraco.Forms.Core.FieldPreValueSourceType` and implement the class.
+
+The following example shows an illustrative custom prevalue source type that returns a hard-coded list of values. It can be extended for your needs via injection of services via the constructor. (See additional example at the bottom.)
+
+Dynamic settings can be applied and validated as shown in the [Validate type settings with ValidateSettings()](adding-a-type.md#validate-type-settings-with-validatesettings) article.
+
+```csharp
+using System;
+using System.Collections.Generic;
+using Umbraco.Forms.Core;
+using Umbraco.Forms.Core.Models;
+
+namespace MyFormsExtensions
+{
+ public class FixedListPrevalueSource : FieldPreValueSourceType
+ {
+ public FixedListPrevalueSource()
+ {
+ Id = new Guid("42C8158D-2AA8-4621-B653-6A63C7545768");
+ Name = "Fixed List";
+ Description = "Example prevalue source providing a fixed list of values.";
+ }
+
+ public override List GetPreValues(Field field, Form form) =>
+ new List
+ {
+ new PreValue
+ {
+ Id = 1,
+ Value = "item-one",
+ Caption = "Item One"
+ },
+ new PreValue
+ {
+ Id = 2,
+ Value = "item-two",
+ Caption = "Item Two"
+ }
+ };
+
+ ///
+ public override List ValidateSettings()
+ {
+ // this is used to validate any dynamic settings you might apply to the PreValueSource
+ // if there are no dynamic settings, return an empty list of Exceptions:
+ var exceptions = new List();
+ return exceptions;
+ }
+ }
+}
+```
+
+You will then need to register this new prevalue source type as a dependency.
+
+```csharp
+using Umbraco.Cms.Core.Composing;
+using Umbraco.Cms.Core.DependencyInjection;
+using Umbraco.Forms.Core.Providers;
+
+namespace MyFormsExtensions
+{
+ public class Startup : IComposer
+ {
+ public void Compose(IUmbracoBuilder builder)
+ {
+ builder.WithCollectionBuilder()
+ .Add();
+ }
+ }
+}
+```
+
+{% hint style="info" %}
+The `PreValue` model in Umbraco Forms Versions 8.13.0, 9.5.0, 10.1.0, and above includes a `.Caption` property. This property is set separately from the `.Value` property. In the previous versions, the `Value` is generally used as the caption when rendered on the form.
+{% endhint %}
+
+## Another Example Using Dependency Injection to Access Additional Services
+
+This example will take a user-provided Content Node and create a custom Prevalue list from the property data on that node. Your own `FieldPreValueSourceType` can get its data from wherever you like - an API call, custom functions, etc.
+
+```csharp
+using System;
+using System.Collections.Generic;
+using Microsoft.Extensions.Logging;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Models.PublishedContent;
+using Umbraco.Cms.Core.Web;
+using Umbraco.Forms.Core;
+using Umbraco.Forms.Core.Models;
+namespace MyFormsExtensions
+ public class FormPrevaluesSourceNode : FieldPreValueSourceType
+ {
+ private readonly ILogger _logger;
+ private readonly IUmbracoContextFactory _UmbracoContextFactory;
+ //DEFINE ANY CONFIGURATION SETTING HERE
+ [Umbraco.Forms.Core.Attributes.Setting(name: "Source Node",
+ Alias = "SourceNodeId",
+ Description = "Node holding the Options desired.",
+ View = "pickers.content")]
+ public string SourceNodeId { get; set; }
+ public FormPrevaluesSourceNode(
+ ILogger logger
+ , IUmbracoContextFactory umbracoContextFactory
+ )
+ {
+ _logger = logger;
+ _UmbracoContextFactory = umbracoContextFactory;
+ this.Id = new Guid("0E4D4E2B-56E1-4E86-84E4-9A0A6051B57C"); //MAKE THIS UNIQUE!
+ this.Name = "Content-defined Form Prevalues Source Node";
+ this.Description = "Select a node of type 'FormPrevaluesSourceNode'";
+ this.Group = "Custom";
+ this.Icon = "icon-science";
+ }
+ ///
+ /// The main method where the PreValues are defined and returned.
+ ///
+ ///
+ ///
+ /// List of 'Umbraco.Forms.Core.Models.PreValue'
+ public override List GetPreValues(Field field, Form form)
+ {
+ List result = new List();
+ try
+ {
+ // Access the Configuration Setting and check that is is valid
+ if (!string.IsNullOrEmpty(SourceNodeId))
+ {
+ var nodeId = 0;
+ var isValidId = Int32.TryParse(SourceNodeId, out nodeId);
+ if (isValidId)
+ {
+ IPublishedContent iPub;
+ using (var umbracoContextReference = _UmbracoContextFactory.EnsureUmbracoContext())
+ {
+ iPub = umbracoContextReference.UmbracoContext.Content.GetById(nodeId);
+ }
+ if (iPub != null)
+ {
+ int sort = 0;
+ //This is using a ModelsBuilder Model to strongly-type the selected node
+ var preValSourceNode = new Models.FormPrevaluesSourceNode(iPub, null);
+ foreach (var prevalue in preValSourceNode.PreValues)
+ {
+ PreValue pv = new PreValue();
+ pv.Id = $"{iPub.Id}-{sort}";
+ pv.Value = prevalue.StoredValue;
+ pv.Caption = prevalue.DisplayText; //.Caption only available in Forms Versions 8.13.0+, 9.5.0+, & 10.1.0+
+ pv.SortOrder = sort;
+ result.Add(pv);
+ sort++;
+ }
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError($"Unable to get options from FormPrevaluesSourceNode #{SourceNodeId}", ex);
+ }
+ return result;
+ }
+ ///
+ /// This is where any checks for Configuration validity are done.
+ /// The exceptions will be displayed in the back-office UI to the user.
+ ///
+ /// List of 'System.Exception'
+ public override List ValidateSettings()
+ {
+ List exceptions = new List();
+ if (string.IsNullOrEmpty(SourceNodeId))
+ {
+ exceptions.Add(new Exception("'Source Node' setting not filled out"));
+ }
+ else
+ {
+ var nodeId = 0;
+ var isValidId = Int32.TryParse(SourceNodeId, out nodeId);
+ if (isValidId)
+ {
+ IPublishedContent iPub;
+ using (var umbracoContextReference = _UmbracoContextFactory.EnsureUmbracoContext())
+ {
+ iPub = umbracoContextReference.UmbracoContext.Content.GetById(nodeId);
+ }
+ if (iPub != null && iPub.ContentType.Alias != Models.FormPrevaluesSourceNode.ModelTypeAlias)
+ {
+ exceptions.Add(new Exception("'Source Node' needs to be of type 'FormPrevaluesSourceNode'"));
+ }
+ }
+ }
+ return exceptions;
+ }
+ }
+}
+```
+
+You will then need to register this new type as a dependency (either in `Program.cs` or in your own IComposer, as shown here).
+
+```csharp
+using Umbraco.Cms.Core.Composing;
+using Umbraco.Cms.Core.DependencyInjection;
+using Umbraco.Forms.Core.Providers;
+namespace MyFormsExtensions
+{
+ public class FormsComposer : IComposer
+ {
+ public void Compose(IUmbracoBuilder builder)
+ {
+ //Adding Custom Form PreValueSource
+ builder.WithCollectionBuilder()
+ .Add();
+ }
+ }
+}
+```
diff --git a/16/umbraco-forms/developer/extending/adding-a-type.md b/16/umbraco-forms/developer/extending/adding-a-type.md
new file mode 100644
index 00000000000..c1b1784e3dc
--- /dev/null
+++ b/16/umbraco-forms/developer/extending/adding-a-type.md
@@ -0,0 +1,222 @@
+# Adding A Type To The Provider Model
+
+To add a new type, no matter if it's a workflow, field, data source, etc, there is a number of tasks to perform to connect to the Forms provider model. This chapter walks through each step and describes how each part works. This chapter will reference the creation of a workflow type. It is, however, the same process for all types.
+
+## Preparations
+
+Create a new class library project in Visual Studio add references to the `Umbraco.Forms.Core.dll` (available via referencing the [NuGet package](https://www.nuget.org/packages/Umbraco.Forms.Core/)). You might also need to reference [Umbraco.Forms.Core.Providers](https://www.nuget.org/packages/Umbraco.Forms.Core.Providers/).
+
+## Adding the type to Forms
+
+The Forms API contains a collection of classes that can be registered at startup or in an Umbraco component. So to add a new type to Forms you inherit from the right class. In the sample below we use the class for the workflow type.
+
+```csharp
+public class LogWorkflow : Umbraco.Forms.Core.WorkflowType
+{
+ private readonly ILogger _logger;
+
+ public LogWorkflow(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ public override WorkflowExecutionStatus Execute(WorkflowExecutionContext context)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override List ValidateSettings() {
+ throw new NotImplementedException();
+ }
+}
+```
+
+When you implement this class you get two methods added. One of them is Execute which performs the execution of the workflow and the other is a method which validates the workflow settings, we will get back to these settings later on.
+
+Any dependencies required that are registered with the dependency injection container can be provided via the constructor.
+
+Even though we have the class inheritance in place, we still need to add a bit of default information.
+
+## Setting up basic type information
+
+Even though we have the class inheritance in place, we still need to add a bit of default information. This information is added in the class's constructor like this:
+
+```csharp
+public LogWorkflow(ILogger logger) {
+
+ _logger = logger;
+
+ this.Name = "The logging workflow";
+ this.Id = new Guid("D6A2C406-CF89-11DE-B075-55B055D89593");
+ this.Description = "This will save an entry to the log";
+}
+```
+
+All three are mandatory and the ID must be unique, otherwise the type might conflict with an existing one.
+
+## Adding settings to a type
+
+Now that we have a basic class setup, we would like to pass setting items to the type. So we can reuse the type on multiple items but with different settings. To add a setting to a type, we add a property to the class, and give it a specific attribute like this:
+
+```csharp
+[Umbraco.Forms.Core.Attributes.Setting("Log Header",
+ Description = "Log item header",
+ View = "TextField")]
+public string LogHeader { get; set; }
+```
+
+The Umbraco.Forms.Core.Attributes.Setting registers the property in Umbraco Forms and there will automatically be UI and storage generated for it. In the attribute, a name, description and the view to be rendered is defined.
+
+With the attribute in place, the property value is set every time the class is instantiated by Umbraco Forms. This means you can use the property in your code like this:
+
+```csharp
+[Umbraco.Forms.Core.Attributes.Setting("Document ID",
+ Description = "Node the log entry belongs to",
+ View = "Pickers.Content")]
+public string Document { get; set; }
+
+public override WorkflowExecutionStatus Execute(WorkflowExecutionContext context) {
+ _logger.LogInformation("Record submitted from: {IP}", context.Record.IP);
+ return WorkflowExecutionStatus.Completed;
+}
+```
+
+For all types that use the provider model, settings work this way. By adding the Setting attribute Forms automatically registers the property in the UI and sets the value when the class is instantiated.
+
+Each setting value is stored as a string with the user interface for generating the value defined via the `View` property.
+
+Umbraco Forms ships with [setting types and you can also create your own](./setting-types.md).
+
+## Validate type settings with ValidateSettings()
+
+The `ValidateSettings()` method which can be found on all types supporting dynamic settings, is used for making sure the data entered by the user is valid and works with the type.
+
+```csharp
+public override List ValidateSettings() {
+ List exceptions = new List();
+ int docId = 0;
+ if (!int.TryParse(Document, out docId))
+ exceptions.Add(new Exception("Document is not a valid integer"));
+ return exceptions;
+}
+```
+
+## Registering the class with Umbraco and Forms
+
+To register the type, ensure your web application project has a reference to the class library, either via a project or NuGet reference.
+Then add the following code into the startup pipeline. In this example, the registration is implemented as an extension method to `IUmbracoBuilder` and should be called from `Program.cs`:
+
+```csharp
+public static IUmbracoBuilder AddUmbracoFormsCustomProviders(this IUmbracoBuilder builder)
+{
+ builder.WithCollectionBuilder()
+ .Add();
+}
+```
+
+An alternative approach is to use a composer, as per this example:
+
+```csharp
+public class UmbracoFormsCustomProvidersComposer : IComposer
+{
+ public void Compose(IUmbracoBuilder builder)
+ {
+ builder.WithCollectionBuilder()
+ .Add();
+ }
+}
+```
+
+There are further convenience methods you can use for registering custom types. These are found in the namespace `Umbraco.Forms.Core.Providers.Extensions`.
+
+For example, instead of the following:
+
+```csharp
+ builder.WithCollectionBuilder()
+ .Add();
+```
+
+Your workflow can be registered using:
+
+```csharp
+ builder.AddFormsWorkflow():
+```
+
+Or:
+
+```csharp
+ builder.FormsWorkflows().Add();
+```
+
+Existing items that are not required in a particular installation can be removed with:
+
+```csharp
+ builder.FormsWorkflows().Exclude();
+```
+
+Also look in the reference chapter for complete class implementations of workflows, fields and export types.
+
+## Overriding default providers in Umbraco Forms
+
+It is possible to override and inherit the original provider, be it a Field Type or Workflow etc. The only requirement when inheriting a fieldtype that you wish to override is to ensure you do not override/change the Id set for the provider, and make sure your class is public.
+
+Here is an example of overriding the Textarea field aka Long Answer.
+
+```csharp
+public class TextareaWithCount : Umbraco.Forms.Core.Providers.FieldTypes.Textarea
+{
+ // Added a new setting when we add our field to the form
+ [Umbraco.Forms.Core.Attributes.Setting("Max length",
+ Description = "Max length",
+ View = "TextField")]
+ public string MaxNumberOfChars { get; set; }
+
+ public TextareaWithCount()
+ {
+ // Set a different view for this fieldtype
+ this.FieldTypeViewName = "FieldType.TextareaWithCount.cshtml";
+
+ // We can change the default name of 'Long answer' to something that suits us
+ this.Name = "Long Answer with Limit";
+ }
+
+ public override IEnumerable ValidateField(Form form, Field field, IEnumerable postedValues, HttpContext context, IPlaceholderParsingService placeholderParsingService, List errors)
+ {
+ var baseValidation = base.ValidateField(form, field, postedValues, context, placeholderParsingService, errors);
+ var value = postedValues.FirstOrDefault();
+
+ if (value != null && value.ToString().Length < int.Parse(MaxNumberOfChars))
+ {
+ return baseValidation;
+ }
+
+ var custom = new List();
+ custom.AddRange(baseValidation);
+ custom.Add("String is way way way too long!");
+
+ return custom;
+ }
+}
+```
+
+As discussed in the previous section, you must also register the extended field type within a composer. You also need to create the the backoffice field type view.
+
+**Composer:**
+
+```csharp
+public class UmbracoFormsCustomProvidersComposer : IComposer
+{
+ public void Compose(IUmbracoBuilder builder)
+ {
+ builder.FormsFields().Add();
+ }
+}
+```
+
+**Backoffice View:**
+
+Add a new HTML file as per the name of the field class (e.g. `textareawithcount.html`) to `\wwwroot\App_Plugins\umbracoforms\Backoffice\Common\FieldTypes\` within your project. For this example, we can copy the original `textarea.html` file used by the standard 'Long Answer' field.
+
+The AngularJS client-side files are shipped with Umbraco Forms as part of a Razor Class Library. So you won't find these files on disk when you install the package.
+
+However if you do want to reference them you can view and extract them from the [`Umbraco.Forms.StaticAssets` NuGet package](https://nuget.info/packages/Umbraco.Forms.StaticAssets).
diff --git a/16/umbraco-forms/developer/extending/adding-a-validation-pattern.md b/16/umbraco-forms/developer/extending/adding-a-validation-pattern.md
new file mode 100644
index 00000000000..dc7ace93bb3
--- /dev/null
+++ b/16/umbraco-forms/developer/extending/adding-a-validation-pattern.md
@@ -0,0 +1,61 @@
+---
+description: >-
+ Customize the regular expression based validation patterns available for text fields.
+---
+
+# Adding a Validation Pattern
+
+When creating a text field in Umbraco Forms, a validation pattern in the form of a regular expression can be applied. Default patterns can be removed or re-ordered, and custom ones created and added.
+
+## Provided patterns
+
+Umbraco Forms ships with three patterns: number, email, and URL. The class names are `Number`, `Email`, and `Url` respectively, and all are found in the
+`Umbraco.Forms.Core.Providers.ValidationPatterns` namespace.
+
+## Creating a custom validation pattern
+
+To create a custom format function, create a class that implements `IValidationPattern`. You will need to initialize five properties:
+
+- `Alias` - an alias that should be unique across the patterns and is typically camel-cased with no spaces.
+- `Name` - the name of the pattern that will be visible in the backoffice.
+- `LabelKey` - as an alternative to providing a name, a translation key can be provided. This will be used to look-up the name in the correct language for the backoffice user.
+- `Pattern` - the regular expression pattern.
+- `ReadOnly` - a flag indicating whether the pattern can be edited in the backoffice.
+
+The following example shows the implementation of a pattern for a United Kingdom postcode (credit for the [pattern](https://stackoverflow.com/a/69806181/489433) to [Mecanik](https://stackoverflow.com/users/6583298/mecanik) at StackOverflow).
+
+```csharp
+using Umbraco.Forms.Core.Interfaces;
+namespace Umbraco.Forms.TestSite.Business.ValidationPatterns
+{
+ public class UkPostCode : IValidationPattern
+ {
+ public string Alias => "ukPostCode";
+ public string Name => "UK Post Code";
+ public string LabelKey => string.Empty;
+ public string Pattern => @"^([a-zA-Z]{1,2}[a-zA-Z\d]{1,2})\s(\d[a-zA-Z]{2})$";
+ public bool ReadOnly => true;
+ }
+}
+```
+
+## Registering the validation pattern
+
+As with other provider types, the validation pattern needs to be registered. There are options to add, remove, and re-order patterns.
+
+An example registration using the `IUmbracoBuilder` is shown below:
+
+```csharp
+public static IUmbracoBuilder AddCustomProviders(this IUmbracoBuilder builder)
+{
+ builder.FormsValidationPatterns()
+ .Append();
+ return builder;
+}
+```
+
+## Using the pattern
+
+With the pattern registered it will be available for selection by editors in the backoffice when they create validation for fields supporting this feature.
+
+
\ No newline at end of file
diff --git a/16/umbraco-forms/developer/extending/adding-a-workflowtype.md b/16/umbraco-forms/developer/extending/adding-a-workflowtype.md
new file mode 100644
index 00000000000..019f24f030d
--- /dev/null
+++ b/16/umbraco-forms/developer/extending/adding-a-workflowtype.md
@@ -0,0 +1,119 @@
+# Adding a workflow type to Umbraco Forms
+
+*This builds on the "[adding a type to the provider model](adding-a-type.md)" chapter*
+
+Add a new class to your project and have it inherit from `Umbraco.Forms.Core.WorkflowType`, and implement the class. For this sample, we will focus on the execute method. This method processes the current record (the data submitted by the form) and have the ability to change data and state.
+
+```csharp
+using Serilog;
+using System;
+using System.Collections.Generic;
+using Umbraco.Forms.Core;
+using Umbraco.Forms.Core.Data.Storage;
+using Umbraco.Forms.Core.Enums;
+using Umbraco.Forms.Core.Persistence.Dtos;
+using Microsoft.Extensions.Logging;
+using Umbraco.Core.Composing;
+
+namespace MyFormsExtensions
+{
+ public class TestWorkflow : WorkflowType
+ {
+ private readonly ILogger _logger;
+
+ public TestWorkflow(ILogger logger)
+ {
+ _logger = logger;
+
+ this.Id = new Guid("ccbeb0d5-adaa-4729-8b4c-4bb439dc0202");
+ this.Name = "TestWorkflow";
+ this.Description = "This workflow is just for testing";
+ this.Icon = "icon-chat-active";
+ this.Group = "Services";
+ }
+
+ public override Task ExecuteAsync(WorkflowExecutionContext context)
+ {
+ // first we log it
+ _logger.LogDebug("the IP " + context.Record.IP + " has submitted a record");
+
+ // we can then iterate through the fields
+ foreach (RecordField rf in context.Record.RecordFields.Values)
+ {
+ // and we can then do something with the collection of values on each field
+ List vals = rf.Values;
+
+ // or get it as a string
+ rf.ValuesAsString(false);
+ }
+
+ //Change the state
+ context.Record.State = FormState.Approved;
+
+ _logger.LogDebug("The record with unique id {RecordId} that was submitted via the Form {FormName} with id {FormId} has been changed to {RecordState} state",
+ context.Record.UniqueId, context.Form.Name, context.Form.Id, "approved");
+
+ return Task.FromResult(WorkflowExecutionStatus.Completed);
+ }
+
+ public override List ValidateSettings()
+ {
+ return new List();
+ }
+ }
+}
+```
+
+## Information available to the workflow
+
+### Record information
+
+The `ExecuteAsync()` method gets a `WorkflowExecutionContext` which has properties for the related `Form`, `Record`, and `FormState`. This parameter contains all information related to the workflow.
+
+The `Record` contains all data and metadata submitted by the form. As shown in the example above, you can iterate over all `RecordField` values in the form. You can also retrieve a specific record field by alias using the following method:
+
+```csharp
+RecordField? recordField = context.Record.GetRecordFieldByAlias("myalias");
+```
+
+Having obtained a reference to a record field, the submitted value can be retrieved via:
+
+```csharp
+var fieldValue = recordField.ValuesAsString(false);
+```
+
+The `ValuesAsString` will JSON escape the result by default. If you do not want this escaping to occur, pass `false` as the parameter.
+
+If the field stores multiple values, they are delimited with a comma. In many cases, you can safely split on that delimiter to obtain the individual values. However, this can lead to issues if the prevalues being selected also contain commas. If that's a concern, the following extension method is available in `Umbraco.Forms.Core.Extensions` to correctly parse the selected prevalues:
+
+```csharp
+IEnumerable selectedPrevalues = recordField.GetSelectedPrevalues();
+```
+
+### Form and state information
+
+The `Form` references the form the record is from and `FormState` provides its state (submitted or approved).
+
+Other context, such as the current `HttpContext`, if needed can be passed as constructor parameters (for example: the `HttpContext` can be accessed by injecting `IHttpContextAccessor`).
+
+## Registering the workflow type
+
+To use the new workflow type, you will need to register it as part of application startup.
+
+```csharp
+using Umbraco.Cms.Core.Composing;
+using Umbraco.Cms.Core.DependencyInjection;
+using Umbraco.Forms.Core.Providers;
+
+namespace MyFormsExtensions
+{
+ public class Startup : IComposer
+ {
+ public void Compose(IUmbracoBuilder builder)
+ {
+ builder.WithCollectionBuilder()
+ .Add();
+ }
+ }
+}
+```
diff --git a/16/umbraco-forms/developer/extending/adding-an-event-handler.md b/16/umbraco-forms/developer/extending/adding-an-event-handler.md
new file mode 100644
index 00000000000..0a6736dcdd9
--- /dev/null
+++ b/16/umbraco-forms/developer/extending/adding-an-event-handler.md
@@ -0,0 +1,181 @@
+---
+description: "See an example of validating a form server-side"
+---
+
+# Adding A Server-Side Notification Handler To Umbraco Forms
+
+## Form validation notification
+
+Add a new class to your project as a handler for the `FormValidateNotification` notification:
+
+```csharp
+using System.Linq;
+using Microsoft.AspNetCore.Http;
+using Umbraco.Cms.Core.Events;
+using Umbraco.Forms.Core.Models;
+using Umbraco.Forms.Core.Services.Notifications;
+
+namespace MyFormsExtensions
+{
+ ///
+ /// Catch form submissions before being saved and perform custom validation.
+ ///
+ public class FormValidateNotificationHandler : INotificationHandler
+ {
+ public void Handle(FormValidateNotification notification)
+ {
+ // If needed, be selective about which form submissions you affect.
+ if (notification.Form.Name == "Form Name")
+ {
+ // Check the ModelState
+ if (notification.ModelState.IsValid == false)
+ {
+ return;
+ }
+
+ // A sample validation
+ var email = GetPostFieldValue(notification.Form, notification.Context, "email");
+ var emailConfirm = GetPostFieldValue(notification.Form, notification.Context, "verifyEmail");
+
+ // If the validation fails, return a ModelError
+ if (email.ToLower() != emailConfirm.ToLower())
+ {
+ notification.ModelState.AddModelError(GetPostField(notification.Form, "verifyEmail").Id.ToString(), "Email does not match");
+ }
+ }
+ }
+
+ private static string GetPostFieldValue(Form form, HttpContext context, string key)
+ {
+ Field field = GetPostField(form, key);
+ if (field == null)
+ {
+ return string.Empty;
+ }
+
+
+ return context.Request.HasFormContentType && context.Request.Form.Keys.Contains(field.Id.ToString())
+ ? context.Request.Form[field.Id.ToString()].ToString().Trim()
+ : string.Empty;
+ }
+
+ private static Field GetPostField(Form form, string key) => form.AllFields.SingleOrDefault(f => f.Alias == key);
+ }
+}
+```
+
+The handler will check the `ModelState` and `Form` field values provided in the notification. If validation fails, we add a `ModelError`.
+
+To register the handler, add the following code into the startup pipeline. In this example, the registration is implemented as an extension method to `IUmbracoBuilder` and should be called from `Program.cs`:
+
+```csharp
+public static IUmbracoBuilder AddUmbracoFormsCoreProviders(this IUmbracoBuilder builder)
+{
+ builder.AddNotificationHandler();
+}
+```
+
+## Service notifications
+
+The services available via interfaces `IFormService`, `IFolderService`, `IDataSourceService` and `IPrevalueSourceService` trigger following notifications before or after an entity handled by the service is modified.
+
+The "-ing" events allow for the entity being changed to be modified before the operation takes place, or to cancel the operation. The "-ed" events fire after the update is complete.
+
+Both can be wired up using a composer and component:
+
+```csharp
+ public class TestSiteComposer : IComposer
+ {
+ public void Compose(IUmbracoBuilder builder)
+ {
+ builder.AddNotificationHandler();
+ }
+ }
+
+ public class FormSavingNotificationHandler : INotificationHandler
+ {
+ public void Handle(FormSavingNotification notification)
+ {
+ foreach (Form form in notification.SavedEntities)
+ {
+ foreach (Page page in form.Pages)
+ {
+ foreach (FieldSet fieldset in page.FieldSets)
+ {
+ foreach (FieldsetContainer fieldsetContainer in fieldset.Containers)
+ {
+ foreach (Field field in fieldsetContainer.Fields)
+ {
+ field.Caption += " (updated)";
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+```
+
+When a form or folder is _moved_ there is no specific service event. However, information available in the `State` dictionary on the notification object can be used to determine whether the item was moved. If so, it can show where it was moved from:
+
+```csharp
+ public class TestSiteComposer : IComposer
+ {
+ public void Compose(IUmbracoBuilder builder)
+ {
+ builder.AddNotificationHandler();
+ }
+ }
+
+ public class FormSavingNotificationHandler : INotificationHandler
+ {
+ private readonly ILogger _logger;
+
+ public FormSavingNotificationHandler(ILogger logger) => _logger = logger;
+
+ public void Handle(FormSavingNotification notification)
+ {
+ foreach (Form savedEntity in notification.SavedEntities)
+ {
+ _logger.LogInformation($"Form updated. New parent: {savedEntity.FolderId}. Old parent: {notification.State["MovedFromFolderId"]}");
+ }
+ }
+ }
+```
+
+If a folder is being moved, the key within the `State` dictionary is `"MovedFromParentId"`.
+
+## Backoffice entry rendering events
+
+When an entry for a form is rendered in the backoffice, an event is available to allow modification of the record detail. This event is available before the record details are presented to the user. This is shown in the following example:
+
+```csharp
+ public class TestSiteComposer : IComposer
+ {
+ public void Compose(IUmbracoBuilder builder)
+ {
+ builder.AddNotificationHandler();
+ }
+ }
+
+ public class EntrySearchResultFetchingNotificationHandler : INotificationHandler
+ {
+ public void Handle(EntrySearchResultFetchingNotification notification)
+ {
+ var transformedFields = new List();
+ foreach (var field in notification.EntrySearchResult.Fields)
+ {
+ if (field?.ToString() == "Test")
+ {
+ transformedFields.Add("Test (updated)");
+ }
+ else
+ {
+ transformedFields.Add(field);
+ }
+ }
+
+ notification.EntrySearchResult.Fields = transformedFields;
+ }
+ }
+```
diff --git a/16/umbraco-forms/developer/extending/customize-default-workflows.md b/16/umbraco-forms/developer/extending/customize-default-workflows.md
new file mode 100644
index 00000000000..cd420da3ca3
--- /dev/null
+++ b/16/umbraco-forms/developer/extending/customize-default-workflows.md
@@ -0,0 +1,257 @@
+---
+description: "How to amend the built-in behavior of adding fields and associating workflows with new forms"
+---
+
+# Customize Default Fields and Workflows For a Form
+
+By default, a single workflow is added when a new form is created. This workflow will send a copy of the form to the email address of the current backoffice user.
+
+A single "data consent" field will also be added unless it has been disabled via configuration.
+
+It's possible to amend this behavior and change it to fit your needs.
+
+## Implementing a Custom Behavior
+
+Two interfaces are used to abstract the logic for setting default fields and workflows for a form. They are `IApplyDefaultFieldsBehavior` and `IApplyDefaultWorkflowsBehavior` respectively.
+
+The default behaviors are defined using built-in, internal classes that implement this interface.
+
+You can create your own implementation of these interfaces.
+
+### Example - Providing a Custom Apply Workflows Behavior
+
+An illustrative example, adding a custom workflow that writes to the log, is shown below.
+
+Firstly, the custom workflow:
+
+```csharp
+using System;
+using System.Collections.Generic;
+using Umbraco.Core.Composing;
+using Umbraco.Core.Logging;
+using Umbraco.Forms.Core.Attributes;
+using Umbraco.Forms.Core.Enums;
+using Umbraco.Forms.Core.Persistence.Dtos;
+
+namespace MyNamespace
+{
+ public class LogMessageWorkflow : WorkflowType
+ {
+ public const string LogMessageWorkflowId = "7ca500a7-cb34-4a82-8ae9-2acac777382d";
+ private readonly ILogger _logger;
+
+ public LogMessageWorkflow(ILogger logger)
+ {
+ Id = new Guid(LogMessageWorkflowId);
+ Name = "Test Workflow";
+ Description = "A test workflow that writes a log line";
+ Icon = "icon-edit";
+
+ _logger = logger;
+ }
+
+ [Setting("Message", Description = "The log message to write", View = "TextField")]
+ public string Message { get; set; }
+
+ public override List ValidateSettings()
+ {
+ var exs = new List();
+ if (string.IsNullOrEmpty(Message))
+ {
+ exs.Add(new Exception("'Message' setting has not been set"));
+ }
+
+ return exs;
+ }
+
+ public override WorkflowExecutionStatus Execute(WorkflowExecutionContext context)
+ {
+ _logger.LogInformation($"'{Message}' written at {DateTime.Now}");
+ return WorkflowExecutionStatus.Completed;
+ }
+ }
+}
+```
+
+Secondly, the custom implementation of `IApplyDefaultWorkflowsBehavior`:
+
+```csharp
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Umbraco.Cms.Core.Hosting;
+using Umbraco.Forms.Core;
+using Umbraco.Forms.Core.Enums;
+using Umbraco.Forms.Core.Providers;
+using Umbraco.Forms.Web.Behaviors;
+using Umbraco.Forms.Web.Models.Backoffice;
+
+namespace MyNamespace
+{
+ public class CustomApplyDefaultWorkflowsBehavior : IApplyDefaultWorkflowsBehavior
+ {
+ private readonly WorkflowCollection _workflowCollection;
+ private readonly IHostingEnvironment _hostingEnvironment;
+
+ public CustomApplyDefaultWorkflowsBehavior(
+ WorkflowCollection workflowCollection, IHostingEnvironment hostingEnvironment)
+ {
+ _workflowCollection = workflowCollection;
+ _hostingEnvironment = hostingEnvironment;
+ }
+
+ public void ApplyDefaultWorkflows(FormDesign form)
+ {
+ // Retrieve the type of the default workflow to add.
+ WorkflowType testWorkflowType = _workflowCollection[new Guid(LogMessageWorkflow.LogMessageWorkflowId)];
+
+ // Create a workflow object based on the workflow type.
+ var defaultWorkflow = new FormWorkflowWithTypeSettings
+ {
+ Id = Guid.Empty,
+ Name = "Log a message",
+ Active = true,
+ IncludeSensitiveData = IncludeSensitiveData.False,
+ SortOrder = 1,
+ WorkflowTypeId = testWorkflowType.Id,
+ WorkflowTypeName = testWorkflowType.Name,
+ WorkflowTypeDescription = testWorkflowType.Description,
+ WorkflowTypeGroup = testWorkflowType.Group,
+ WorkflowTypeIcon = testWorkflowType.Icon,
+
+ // Optionally set the default workflow to be mandatory (which means editors won't be able to remove it
+ // via the back-office user interface).
+ IsMandatory = true
+ };
+
+ // Retrieve the settings from the type.
+ Dictionary workflowTypeSettings = testWorkflowType.Settings();
+
+ // Create a collection for the specific settings to be applied to the workflow.
+ // Populate with the setting details from the type.
+ var workflowSettings = new List();
+ foreach (KeyValuePair setting in workflowTypeSettings)
+ {
+ Core.Attributes.Setting settingItem = setting.Value;
+
+ var settingItemToAdd = new SettingWithValue
+ {
+ Name = settingItem.Name,
+ Alias = settingItem.Alias,
+ Description = settingItem.Description,
+ Prevalues = settingItem.GetPreValues(),
+ View = _hostingEnvironment.ToAbsolute(settingItem.GetSettingView()),
+ Value = string.Empty
+ };
+
+ workflowSettings.Add(settingItemToAdd);
+ }
+
+ // For each setting, provide a value for the workflow instance (in this example, we only have one).
+ SettingWithValue messageSetting = workflowSettings.SingleOrDefault(x => x.Alias == "Message");
+ if (messageSetting != null)
+ {
+ messageSetting.Value = "A test log message";
+ }
+
+ // Apply the settings to the workflow.
+ defaultWorkflow.Settings = workflowSettings;
+
+ // Associate the workflow with the appropriate form submission event.
+ form.FormWorkflows.OnSubmit.Add(defaultWorkflow);
+ }
+ }
+}
+```
+
+Finally, to register the custom implementation in place of the default one:
+
+```csharp
+using Umbraco.Cms.Core.Composing;
+using Umbraco.Cms.Core.DependencyInjection;
+using Umbraco.Extensions;
+using Umbraco.Forms.Core.Providers;
+using Umbraco.Forms.Testsite.Business.Workflows;
+using Umbraco.Forms.Web.Behaviors;
+
+namespace MyNamespace
+{
+ public class TestSiteComposer : IComposer
+ {
+ public void Compose(IUmbracoBuilder builder)
+ {
+ builder.WithCollectionBuilder()
+ .Add();
+
+ builder.Services.AddUnique();
+ }
+ }
+}
+```
+
+#### Setting a Mandatory Default Workflow
+
+When adding a default workflow in code, it's possible to make it mandatory, which will prevent editors from removing it from a form.
+
+You can see this in the example above, where the `IsMandatory` property of the created `FormWorkflowWithTypeSettings` instance is set to `true`.
+
+### Example - Providing a Custom Apply Fields Behavior
+
+The following class shows the default implementation provided with Forms. You can copy this and customize it to your needs.
+
+```csharp
+using Microsoft.Extensions.Options;
+using Umbraco.Forms.Core.Configuration;
+using Umbraco.Forms.Core.Models;
+using Umbraco.Forms.Web.Extensions;
+using Umbraco.Forms.Web.Models.Backoffice;
+
+namespace Umbraco.Forms.Web.Behaviors
+{
+ internal class CustomApplyDefaultFieldsBehavior : IApplyDefaultFieldsBehavior
+ {
+ private readonly FormDesignSettings _formDesignSettings;
+
+ public CustomApplyDefaultFieldsBehavior(IOptions formDesignSettings) =>
+ _formDesignSettings = formDesignSettings.Value;
+
+ public virtual void ApplyDefaultFields(FormDesign form)
+ {
+ // Add one page as a starting point.
+ var page = new Page();
+ form.Pages.Add(page);
+
+ // Add one empty fieldset to the page to start with.
+ var fieldset = new FieldSet
+ {
+ Id = Guid.NewGuid()
+ };
+ page.FieldSets.Add(fieldset);
+
+ // Add one full-width (12cols) container/row to the fieldset.
+ var container = new FieldsetContainer
+ {
+ Width = 12
+ };
+ fieldset.Containers.Add(container);
+
+ // As all forms default to having StoreRecordsLocally we need to add the data consent field to the the form
+ // (unless this feature has been explicitly disabled).
+ if (_formDesignSettings.DisableAutomaticAdditionOfDataConsentField)
+ {
+ return;
+ }
+
+ container.AddDataConsentField(_formDesignSettings, _fieldCollection);
+
+ // Add any further fields you require.
+ }
+ }
+}
+```
+
+Again, you will need to register your custom class, for example, in a composer with:
+
+```csharp
+builder.Services.AddUnique();
+```
diff --git a/16/umbraco-forms/developer/extending/excluding-a-built-in-field.md b/16/umbraco-forms/developer/extending/excluding-a-built-in-field.md
new file mode 100644
index 00000000000..9714474a3e0
--- /dev/null
+++ b/16/umbraco-forms/developer/extending/excluding-a-built-in-field.md
@@ -0,0 +1,28 @@
+# Excluding a Built-in Field
+
+Umbraco Forms comes with some built-in fields however it is possible to exclude/remove them if necessary.
+There might some use cases where you have no use for file upload and don't want editors using them. Or perhaps you want to remove a field to replace it with one with enhanced functionality that you build yourself.
+
+## Example
+
+The following class shows how to exclude built-in field types using a custom composer. The `Password`, `Recaptcha2` and `RichText` field types (or "answers") will no longer be available for selection when creating a form in the backoffice.
+
+```csharp
+using Umbraco.Cms.Core.Composing;
+using Umbraco.Forms.Core.Providers.Extensions;
+using Umbraco.Forms.Core.Providers.FieldTypes;
+
+namespace MyNamespace
+{
+ public class MyFormFieldsComposer : IComposer
+ {
+ public void Compose(IUmbracoBuilder builder)
+ {
+ builder.FormsFields()
+ .Exclude()
+ .Exclude()
+ .Exclude();
+ }
+ }
+}
+```
diff --git a/16/umbraco-forms/developer/extending/images/text-with-field-picker.png b/16/umbraco-forms/developer/extending/images/text-with-field-picker.png
new file mode 100644
index 00000000000..bc822cbc599
Binary files /dev/null and b/16/umbraco-forms/developer/extending/images/text-with-field-picker.png differ
diff --git a/16/umbraco-forms/developer/extending/setting-types.md b/16/umbraco-forms/developer/extending/setting-types.md
new file mode 100644
index 00000000000..2c816b49142
--- /dev/null
+++ b/16/umbraco-forms/developer/extending/setting-types.md
@@ -0,0 +1,63 @@
+# Setting Types
+
+Umbraco Forms field, prevalue source and workflow types are defined in C# and include one or more setting values.
+
+These settings are completed by the editor when using the type on their form.
+
+Each setting type can have it's own user interface. So a string can use a text box but a more complicated JSON structure can use a more appropriate user interface.
+
+From Forms 14, each interface is defined as an Umbraco [property editor UI](https://docs.umbraco.com/umbraco-cms/extending/property-editors/composition/property-editor-ui).
+
+The user interface used for a particular setting is defined by the `View` property:
+
+```csharp
+[Umbraco.Forms.Core.Attributes.Setting("Message", View = "Umb.PropertyEditorUi.TextBox")]
+public string Message { get; set; }
+```
+
+If not specified, the default `Umb.PropertyEditorUi.TextBox` is used.
+
+## Built-in setting types
+
+The following setting types are available and are used for the field, prevalue source and workflow types that ship with the package.
+
+Some are defined with the Umbraco CMS and some ship with the Forms package.
+
+| Name | Source | Description | Used in |
+|--------------------------------------------------|--------------|-------------------------------------------------------------------|-------------------------------------------------|
+| Umb.PropertyEditorUi.ContentPicker.Source | CMS | Uses a content picker with the option for XPath entry | The "Save as Umbraco node" workflow |
+| Umb.PropertyEditorUi.Dropdown | CMS | Used for selection from a list of options | |
+| Umb.PropertyEditorUi.Integer | CMS | Uses numerical text box for entry | |
+| Umb.PropertyEditorUi.MediaEntityPicker | CMS | Uses a media item picker for entry | The "Send email with XSLT template" workflow |
+| Umb.PropertyEditorUi.MultipleTextString | CMS | Uses multiple text boxes for entry | Not used in core types |
+| Umb.PropertyEditorUi.Slider | CMS | Uses a slider for range input | The "reCAPTCHAv3" field type |
+| Umb.PropertyEditorUi.TextArea | CMS | Uses a multiline textbox for entry | |
+| Umb.PropertyEditorUi.TextBox | CMS | Uses a single-line textbox for entry | |
+| Umb.PropertyEditorUi.TinyMCE | CMS | Uses a rich text editor for input | The "Send email" workflows |
+| Umb.PropertyEditorUi.Toggle | CMS | Uses a single checkbox for entry | |
+| Umb.PropertyEditorUi.UploadField | CMS | Used for selection of a file | The "Text file" prevalue source |
+| Forms.PropertyEditorUi.DataTypePicker | Forms | Uses a datatype picker | The "Umbraco prevalues" prevalue source |
+| Forms.PropertyEditorUi.DocumentTypePicker | Forms | Uses a Document Type picker | The "Umbraco nodes" prevalue source |
+| Forms.PropertyEditorUi.DocumentTypeFieldPicker | Forms | Uses to select fields from a Document Type | The "Umbraco nodes" prevalue source |
+| Forms.PropertyEditorUi.DocumentMapper | Forms | Used for mapping of fields from a Document Type | The "Save as Umbraco node" workflow |
+| Forms.PropertyEditorUi.EmailTemplatePicker | Forms | Used for selection of an email template | The "Send email with Razor template" workflow |
+| Forms.PropertyEditorUi.FieldMapper | Forms | Used to map fields from a form to required aliases | The "Send to URL" workflow |
+| Forms.PropertyEditorUi.Password | Forms | Uses password text box for entry | |
+| Forms.PropertyEditorUi.StandardFieldMapper | Forms | Used to map system fields from a form to required aliases | The "Send to URL" workflow |
+| Forms.PropertyEditorUi.TextWithFieldPicker | Forms | Uses a single-line textbox/form field list for entry | Not used in core types |
+
+Most of the above setting types are used in one or more field, prevalue source and workflow types available with Umbraco Forms. For the less common ones, a usage has been indicated in the table.
+
+## Additional setting types
+
+Some types we don't use within the package, but we make available for developers to use when creating their own types.
+
+For example `Forms.PropertyEditorUi.TextWithFieldPicker`. This offers the option of text field entry or the selection of a field from the form. This can be useful in workflows where you need to reference the value of a specific field.
+
+
+
+## Creating a setting type
+
+It's also possible to define your own setting type using a combination of server and client-side code.
+
+Read how do this in the article on [adding a field type](./adding-a-fieldtype.md#field-settings).
diff --git a/16/umbraco-forms/developer/field-types.md b/16/umbraco-forms/developer/field-types.md
new file mode 100644
index 00000000000..e6f058dd377
--- /dev/null
+++ b/16/umbraco-forms/developer/field-types.md
@@ -0,0 +1,31 @@
+# Field Types
+
+Umbraco Forms comes with a number of Field Types to allow you to request certain data in the forms that you design & build. This documentation is to guide specific details about field types that we ship that require some detail in how they work.
+
+## Date Picker
+
+The date picker uses a front-end library called [PikaDay.js](https://github.com/dbushell/Pikaday) to display a UI to pick dates from. We have added the support for the Pikaday date picker to be localized based on the page the form is rendered on. This displays the picked date in the correct locale. In JavaScript, we update a hidden field with a standard date format. This is done to send the date to the server, ensuring the record submission is stored in a standard format. This is to avoid locale mixing up dates.
+
+To achieve this a new Razor partial view is included `/Views/Partials/Forms/DatePicker.cshtml`. Once on a page with a form that includes a Date Picker, it also includes the MomentJS library to assist with date locale formatting. Additionally, there are appropriate changes to Pikaday.js to support the locales. If you wish to use a different DatePicker component this is the file that you would customize to your needs.
+
+### Date Picker configuration of the year range
+
+The `DatePicker` has one configuration setting to control the number of year shown. The default is 10 years which makes the picker unusable for picking birth dates.
+
+Go to your `appsettings.json` and add:
+```json
+ "Umbraco": {
+ "CMS": {
+ ...
+ },
+ "Forms": {
+ "FieldTypes": {
+ "DatePicker": {
+ "DatePickerYearRange": 12
+ }
+ }
+ }
+ }
+```
+
+You can then change the `DatePickerYearRange` to a higher number (for example 100).
diff --git a/16/umbraco-forms/developer/forms-in-the-database.md b/16/umbraco-forms/developer/forms-in-the-database.md
new file mode 100644
index 00000000000..e49ef383ec9
--- /dev/null
+++ b/16/umbraco-forms/developer/forms-in-the-database.md
@@ -0,0 +1,53 @@
+# Umbraco Forms in the Database
+
+In Umbraco Forms, it is _only_ possible to store Form data in the database.
+
+If you are upgrading to Umbraco 9 or later and using Forms, you should first migrate the Forms to the database using Forms 8. As of Umbraco Forms version 8.5.0 it is possible to persist all Forms data in the Umbraco database. This includes definitions for each Form and their fields, as well as workflow definitions and prevalues.
+
+{% hint style="info" %}
+**Custom file system providers**
+
+If [custom file system providers are used on your project for storing Umbraco Forms data](https://docs.umbraco.com/umbraco-cms/extending/filesystemproviders#custom-providers), the migration will not be able to run.
+
+To persist your Umbraco Forms data in the database, you will need to revert to a **standard Umbraco Forms configuration**. Use the default provider to store the Forms definition files in the default location.
+
+You need to ensure that your Forms definition files are moved from their previous location. This is a non-default file path, blob storage, or similar to the default location, `App_Data/UmbracoForms`, that Forms will now be using.
+
+Your configuration is now considered a standard configuration and you can perform the steps required for a normal migration.
+{% endhint %}
+
+## Enable storing Forms definitions in the database
+
+To persist Umbraco Forms definitions in the database, follow these steps:
+
+1. Upgrade to at least Umbraco Forms version 8.5.2.
+2. Open the configuration file `App_Plugins\UmbracoForms\UmbracoForms.config`.
+3. Locate the `StoreUmbracoFormsInDb` key in the `` section, and make sure it has the following value:
+
+ ```xml
+
+ ```
+
+4. Save the file.
+
+If you are working with a Umbraco Cloud project, make sure you follow the migration steps outlined in the [Umbraco Forms on Cloud](https://docs.umbraco.com/umbraco-cloud/deployments/umbraco-forms-on-cloud) article.
+
+{% hint style="warning" %}
+Enabling the persisting of Umbraco Forms in the database is irreversible. Once you've made the change, reverting to the file approach will not be an option.
+{% endhint %}
+
+When you save the file, the site will restart and run a migration step, migrating the files to the Umbraco database.
+
+## Migrating Forms in files into a site
+
+You can force Forms to rerun the migration of the file-format Forms if you have a Umbraco 8 site storing Forms in the database.
+
+First of all, you should ensure that you have enabled the setting that persists Forms in the database, as the migration requires this (`StoreUmbracoFormsInDb`) key. We highly recommend testing this on a local setup before applying it to your live site.
+
+1. Copy over the Forms, workflows, prevaluesources, and datasource files to the site into `~\App_Data\UmbracoForms\Data`.
+2. Go to the database and find the `[umbracoKeyValue]` table.
+3. Find the Form's row and check that the value is `1d084819-84ba-4ac7-b152-2c3d167d22bc` (if not you are not currently working with Forms in the database, changing the setting should be enough).
+4. Change that value to `{forms-init-complete}`.
+5. Restart the site.
+
+The site will now try to migrate the Forms files into the database. In the umbracoTraceLog, you can follow the progress. It will throw errors if anything goes wrong. Additionally, it will log out "The Umbraco Forms DB table {TableName} already exists" for the 4 Forms tables before starting the migration.
diff --git a/16/umbraco-forms/developer/healthchecks/README.md b/16/umbraco-forms/developer/healthchecks/README.md
new file mode 100644
index 00000000000..d9b34c2e10b
--- /dev/null
+++ b/16/umbraco-forms/developer/healthchecks/README.md
@@ -0,0 +1,115 @@
+# Health Checks
+
+In this article, you will find information about Umbraco Forms-related health checks that can be run from the Umbraco backoffice to ensure that your installation is running seamlessly.
+
+Read the [Health Check](https://docs.umbraco.com/umbraco-cms/extending/health-check) article to learn more about the feature in general.
+
+## Database Integrity Health Check
+
+Running this health check will verify whether the database tables for the Umbraco Forms installation are all set up correctly with the proper data integrity checks.
+
+In this section, you can learn more about the background for adding this check, as well as how to use and understand the results.
+
+### Background
+
+A health check was introduced to confirm the Umbraco Forms database tables are all set up with the expected data integrity checks - i.e. primary keys, foreign keys and unique constraints.
+
+In most cases, you can expect them all to be in place without any developer intervention. For new installs, the database schema is initialized with all the necessary integrity constraints. And for upgrades, any new schema changes are automatically applied.
+
+There remains the possibility though that not all will be in place for a particular installation. For example, this could happen if a constraint is added in a new version. It can't be added via an automated migration due to existing data integrity issues.
+
+In particular, prior to version 8.7, there were a number of tables that weren't defined as strictly as they should be in this area. So we've added some primary key, foreign key and unique constraints with this version. If you've been running a version prior to this and are upgrading, these schema updates will be applied automatically _unless_ there is existing data in the tables that prevent them from being added.
+
+There shouldn't be - but without these constraints in place it's always possible for an application bug to exist that allows for example the creation of duplicate records, or the orphaning of records, that aren't correct. This is the reason for the constraints to exist, and why we want to ensure they are in place.
+
+### Running The Health Check
+
+To run the health check:
+
+1. Navigate to the **Health Check** dashboard in the **Settings** section in the Umbraco backoffice.
+
+
+2. Click on the **Forms** button and select **Perform checks**. You'll see a result that looks something like this:
+
+
+
+If you have a full set of green ticks, then you're all good - and no need to read on!
+
+If you have one or more red crosses though, that means a particular constraint wasn't able to be applied via the automatic schema migrations when you installed a new version of Umbraco Forms, due to existing data issues.
+
+It isn't essential that they are resolved - the package can and does function correctly without them - but for reasons of ensuring data integrity and performance, it is recommended that they are.
+
+### Resolving Reported Problems
+
+When Umbraco Forms installs an upgrade, it will attempt to apply any schema changes. If though, the update isn't essential, and it can't proceed due to existing data integrity issues, the failed update will be logged and then the rest of the migration will continue.
+
+As well as in the log files, such issues will be visible via the health check and will need to be resolved by applying scripts directly to the database.
+
+To support this, we provide the following SQL scripts:
+
+* Apply database integrity schema changes for 8.7.0+ - [8.7.0-apply-keys-and-indexes](apply-keys.md)
+* Apply database integrity schema changes for 8.7.0+ (Forms in database tables) - [8.7.0-apply-keys-and-indexes-forms-in-db](forms-in-the-database-apply-keys.md)
+
+The first of these provides the SQL statements required to apply the schema updates for 8.7.0+ to the common Umbraco Forms tables. The second applies to those tables used for when Forms are stored in the database, and hence only need to be applied if that option is configured.
+
+{% hint style="info" %}
+Before running any scripts or queries, please be sure to have a database backup in place.
+{% endhint %}
+
+To take an example, let's say that via the health check results you can see that the _"Unique constraint on table 'UFForms', column 'Key' is missing."_
+
+If you look in the SQL script you'll see that in order to apply this directly to the database, you would need to run the following SQL statement:
+
+```sql
+-- Adds unique constraint to UFForms.
+ALTER TABLE dbo.UFForms
+ADD CONSTRAINT UK_UFForms_Key UNIQUE NONCLUSTERED
+(
+ [Key] ASC
+) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
+GO
+```
+
+If you run it though, you'll see the reason why the migration that ran when Umbraco Forms was upgraded couldn't apply the change:
+
+```sql
+The CREATE UNIQUE INDEX statement terminated because a duplicate key was found for the object name 'dbo.UFForms' and the index name 'UK_UFForms_Key'. The duplicate key value is (...).
+```
+
+The constraint can't be applied if there are existing duplicate values, so first they need to be found and removed.
+
+To find duplicate values in the 'Key' field in this table you can run the following SQL statement:
+
+```sql
+SELECT [Key]
+FROM UFForms
+GROUP BY [Key]
+HAVING COUNT(*) > 1
+```
+
+Running the statement above will list out the 'Key' fields that are duplicated in the table.
+
+To see the full details of the duplicate records, you can use this query:
+
+```sql
+SELECT *
+FROM UFForms
+WHERE [Key] IN (SELECT [Key]
+ FROM UFForms
+ GROUP BY [Key]
+ HAVING COUNT(*) > 1
+)
+```
+
+From the `Id` field you can identify the Form records that are duplicated and should be removed, and delete the records. To check you have found them all, run one of the above queries again, and confirm you find no records returned.
+
+Finally you can run the `ALTER TABLE...` statement shown above to apply the constraint, and confirm via the health check that it's now in place.
+
+By repeating similar steps as required, you'll be able to ensure that all recommended keys, constraints and indexes are in place.
+
+If for any reason you wish to revert the changes - perhaps when testing these updates in a non-production environment - reversion scripts for all the 8.7 updates are also provided:
+
+To support this, we provide the following SQL scripts:
+
+* Revert database integrity schema changes for 8.7.0+ - [8.7.0-apply-keys-and-indexes\_revert](apply-keys.md#revert-application-of-keys-and-indexes)
+* Revert database integrity schema changes for 8.7.0+ (Forms in database tables) - [8.7.0-apply-keys-and-indexes-forms-in-db\_revert](forms-in-the-database-apply-keys.md#reverting-the-application-of-keys-and-indexes)
diff --git a/16/umbraco-forms/developer/healthchecks/apply-keys.md b/16/umbraco-forms/developer/healthchecks/apply-keys.md
new file mode 100644
index 00000000000..2a1213bd225
--- /dev/null
+++ b/16/umbraco-forms/developer/healthchecks/apply-keys.md
@@ -0,0 +1,231 @@
+# Apply keys and indexes
+
+```sql
+/*
+ Applies recommended primary keys, foreign keys and indexes to core Umbraco Forms tables.
+ This replicates for SQL Server the migration AddRecordKeysAndIndexes.
+ */
+
+-- Adds relationship between UFRecords and UFRecordFields.
+ALTER TABLE dbo.UFRecordFields
+ADD CONSTRAINT
+ FK_UFRecordFields_UFRecords_Record FOREIGN KEY
+ (
+ Record
+ ) REFERENCES dbo.UFRecords
+ (
+ Id
+ ) ON UPDATE NO ACTION
+ ON DELETE NO ACTION
+GO
+
+-- Adds primary keys to UFRecordData* tables.
+ALTER TABLE dbo.UFRecordDataBit
+ADD CONSTRAINT
+ PK_UFRecordDataBit PRIMARY KEY CLUSTERED
+ (
+ Id
+ ) WITH( STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
+GO
+
+ALTER TABLE dbo.UFRecordDataDateTime
+ADD CONSTRAINT
+ PK_UFRecordDataDateTime PRIMARY KEY CLUSTERED
+ (
+ Id
+ ) WITH( STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
+GO
+
+ALTER TABLE dbo.UFRecordDataInteger
+ADD CONSTRAINT
+ PK_UFRecordDataInteger PRIMARY KEY CLUSTERED
+ (
+ Id
+ ) WITH( STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
+GO
+
+ALTER TABLE dbo.UFRecordDataLongString
+ADD CONSTRAINT
+ PK_UFRecordDataLongString PRIMARY KEY CLUSTERED
+ (
+ Id
+ ) WITH( STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
+GO
+
+-- Adds relationship between UFRecordFields and UFREcordData* tables.
+ALTER TABLE dbo.UFRecordDataBit
+ADD CONSTRAINT
+ FK_UFRecordDataBit_UFRecordFields_Key FOREIGN KEY
+ (
+ [Key]
+ ) REFERENCES dbo.UFRecordFields
+ (
+ [Key]
+ ) ON UPDATE NO ACTION
+ ON DELETE NO ACTION
+GO
+
+ALTER TABLE dbo.UFRecordDataDateTime
+ADD CONSTRAINT
+ FK_UFRecordDataDateTime_UFRecordFields_Key FOREIGN KEY
+ (
+ [Key]
+ ) REFERENCES dbo.UFRecordFields
+ (
+ [Key]
+ ) ON UPDATE NO ACTION
+ ON DELETE NO ACTION
+GO
+
+ALTER TABLE dbo.UFRecordDataInteger
+ADD CONSTRAINT
+ FK_UFRecordDataInteger_UFRecordFields_Key FOREIGN KEY
+ (
+ [Key]
+ ) REFERENCES dbo.UFRecordFields
+ (
+ [Key]
+ ) ON UPDATE NO ACTION
+ ON DELETE NO ACTION
+GO
+
+ALTER TABLE dbo.UFRecordDataLongString
+ADD CONSTRAINT
+ FK_UFRecordDataLongString_UFRecordFields_Key FOREIGN KEY
+ (
+ [Key]
+ ) REFERENCES dbo.UFRecordFields
+ (
+ [Key]
+ ) ON UPDATE NO ACTION
+ ON DELETE NO ACTION
+GO
+
+-- Adds index on foreign key fields in UFREcordData* tables.
+CREATE NONCLUSTERED INDEX IX_UFRecordDataBit_Key ON dbo.UFRecordDataBit
+(
+ [Key] ASC
+) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
+GO
+
+CREATE NONCLUSTERED INDEX IX_UFRecordDataDateTime_Key ON dbo.UFRecordDataDateTime
+(
+ [Key] ASC
+) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
+GO
+
+CREATE NONCLUSTERED INDEX IX_UFRecordDataInteger_Key ON dbo.UFRecordDataInteger
+(
+ [Key] ASC
+) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
+GO
+
+CREATE NONCLUSTERED INDEX IX_UFRecordDataLongString_Key ON dbo.UFRecordDataLongString
+(
+ [Key] ASC
+) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
+GO
+
+-- Adds primary key to UFUserSecurity.
+ALTER TABLE dbo.UFUserSecurity
+ADD CONSTRAINT
+ PK_UFUserSecurity PRIMARY KEY CLUSTERED
+ (
+ [User]
+ ) WITH( STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
+GO
+
+-- Adds primary key to UFUserFormSecurity.
+ALTER TABLE dbo.UFUserFormSecurity
+ADD CONSTRAINT
+ PK_UFUserFormSecurity PRIMARY KEY CLUSTERED
+ (
+ Id
+ ) WITH( STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
+GO
+
+-- Adds unique constraint to UFUserFormSecurity across user/form fields.
+ALTER TABLE dbo.UFUserFormSecurity
+ADD CONSTRAINT UK_UFUserFormSecurity_User_Form UNIQUE NONCLUSTERED
+(
+ [User] ASC,
+ [Form] ASC
+) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
+GO
+```
+
+## Revert application of keys and indexes
+
+```sql
+/*
+ Reverts application of recommended primary keys, foreign keys and indexes to core Umbraco Forms tables.
+ This reverts for SQL Server the migration AddRecordKeysAndIndexes and can be used for rolling that back in testing.
+ */
+
+-- Reverts addition of relationship between UFRecords and UFRecordFields.
+ALTER TABLE dbo.UFRecordFields
+DROP CONSTRAINT IF EXISTS FK_UFRecordFields_UFRecords_Record
+GO
+
+-- Reverts addition of primary keys to UFRecordData* tables.
+ALTER TABLE dbo.UFRecordDataBit
+DROP CONSTRAINT IF EXISTS PK_UFRecordDataBit
+GO
+
+ALTER TABLE dbo.UFRecordDataDateTime
+DROP CONSTRAINT IF EXISTS PK_UFRecordDataDateTime
+GO
+
+ALTER TABLE dbo.UFRecordDataInteger
+DROP CONSTRAINT IF EXISTS PK_UFRecordDataInteger
+GO
+
+ALTER TABLE dbo.UFRecordDataLongString
+DROP CONSTRAINT IF EXISTS PK_UFRecordDataLongString
+GO
+
+-- Reverts addition of relationship between UFRecordFields and UFREcordData* tables.
+ALTER TABLE dbo.UFRecordDataBit
+DROP CONSTRAINT IF EXISTS FK_UFRecordDataBit_UFRecordFields_Key
+GO
+
+ALTER TABLE dbo.UFRecordDataDateTime
+DROP CONSTRAINT IF EXISTS FK_UFRecordDataDateTime_UFRecordFields_Key
+GO
+
+ALTER TABLE dbo.UFRecordDataInteger
+DROP CONSTRAINT IF EXISTS FK_UFRecordDataInteger_UFRecordFields_Key
+GO
+
+ALTER TABLE dbo.UFRecordDataLongString
+DROP CONSTRAINT IF EXISTS FK_UFRecordDataLongString_UFRecordFields_Key
+GO
+
+-- Reverts addition of index on foreign key fields in UFREcordData* tables.
+DROP INDEX IF EXISTS IX_UFRecordDataBit_Key ON dbo.UFRecordDataBit
+GO
+
+DROP INDEX IF EXISTS IX_UFRecordDataDateTime_Key ON dbo.UFRecordDataDateTime
+GO
+
+DROP INDEX IF EXISTS IX_UFRecordDataInteger_Key ON dbo.UFRecordDataInteger
+GO
+
+DROP INDEX IF EXISTS IX_UFRecordDataLongString_Key ON dbo.UFRecordDataLongString
+GO
+
+-- Reverts addition of primary key to UFUserSecurity
+ALTER TABLE dbo.UFUserSecurity
+DROP CONSTRAINT IF EXISTS PK_UFUserSecurity
+GO
+
+-- Reverts addition of primary key to UFUserFormSecurity
+ALTER TABLE dbo.UFUserFormSecurity
+DROP CONSTRAINT IF EXISTS PK_UFUserFormSecurity
+GO
+
+-- Reverts addition of unique constraint to UFUserFormSecurity across user/form fields.
+ALTER TABLE dbo.UFUserFormSecurity
+DROP CONSTRAINT IF EXISTS UK_UFUserFormSecurity_User_Form
+GO
+```
diff --git a/16/umbraco-forms/developer/healthchecks/forms-in-the-database-apply-keys.md b/16/umbraco-forms/developer/healthchecks/forms-in-the-database-apply-keys.md
new file mode 100644
index 00000000000..814bf15930d
--- /dev/null
+++ b/16/umbraco-forms/developer/healthchecks/forms-in-the-database-apply-keys.md
@@ -0,0 +1,86 @@
+# Apply keys and indexes for forms in the database
+
+```sql
+/*
+ Applies recommended primary keys, foreign keys and indexes to Umbraco Forms tables relating to "forms in the database" (i.e.
+ when configuration key StoreUmbracoFormsInDb = true).
+ This replicates for SQL Server the migration AddFormKeysAndIndexes.
+ */
+
+-- Adds unique constraint to UFForms.
+ALTER TABLE dbo.UFForms
+ADD CONSTRAINT UK_UFForms_Key UNIQUE NONCLUSTERED
+(
+ [Key] ASC
+) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
+GO
+
+-- Adds unique constraint to UFDataSource.
+ALTER TABLE dbo.UFDataSource
+ADD CONSTRAINT UK_UFDataSource_Key UNIQUE NONCLUSTERED
+(
+ [Key] ASC
+) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
+GO
+
+-- Adds unique constraint to UFPrevalueSource.
+ALTER TABLE dbo.UFPrevalueSource
+ADD CONSTRAINT UK_UFPrevalueSource_Key UNIQUE NONCLUSTERED
+(
+ [Key] ASC
+) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
+GO
+
+-- Adds unique constraint to UFWorkflows.
+ALTER TABLE dbo.UFWorkflows
+ADD CONSTRAINT UK_UFWorkflows_Key UNIQUE NONCLUSTERED
+(
+ [Key] ASC
+) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
+GO
+
+-- Adds index on join field in UFWorkflows.
+CREATE NONCLUSTERED INDEX IX_UFWorkflows_FormId ON dbo.UFWorkflows
+(
+ FormId ASC
+) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
+GO
+```
+
+## Reverting the application of keys and indexes
+
+```sql
+/*
+ Reverts application of recommended primary keys, foreign keys and indexes to Umbraco Forms tables relating to "forms in the database" (i.e.
+ when configuration key StoreUmbracoFormsInDb = true).
+ This reverts for SQL Server the migration AddFormKeysAndIndexes and can be used for rolling that back in testing.
+ */
+
+-- Reverts addition of unique constraint to UFForms.
+ALTER TABLE dbo.UFForms
+DROP CONSTRAINT IF EXISTS UK_UFForms_Key
+GO
+
+-- Reverts addition of unique constraint to UFPrevalueSource.
+ALTER TABLE dbo.UFDataSource
+DROP CONSTRAINT IF EXISTS UK_UFDataSource_Key
+GO
+
+-- Reverts addition of unique constraint to UFPrevalueSource.
+ALTER TABLE dbo.UFPrevalueSource
+DROP CONSTRAINT IF EXISTS UK_UFPrevalueSource_Key
+GO
+
+-- Reverts addition of unique constraint to UFWorkflows.
+ALTER TABLE dbo.UFWorkflows
+DROP CONSTRAINT IF EXISTS UK_UFWorkflows_Key
+GO
+
+-- Reverts addition of index on foreign key fields in UFWorkflows.
+DROP INDEX IF EXISTS IX_UFWorkflows_FormId ON dbo.UFWorkflows
+GO
+
+-- Reverts addition of index on foreign key fields in UFWorkflows.
+DROP INDEX IF EXISTS IX_UFWorkflows_FormId ON dbo.UFWorkflows
+GO
+```
diff --git a/16/umbraco-forms/developer/healthchecks/images/Umb-backoffice.png b/16/umbraco-forms/developer/healthchecks/images/Umb-backoffice.png
new file mode 100644
index 00000000000..7c006f92ca1
Binary files /dev/null and b/16/umbraco-forms/developer/healthchecks/images/Umb-backoffice.png differ
diff --git a/16/umbraco-forms/developer/healthchecks/images/healthcheck-v14.png b/16/umbraco-forms/developer/healthchecks/images/healthcheck-v14.png
new file mode 100644
index 00000000000..eb16cd812d5
Binary files /dev/null and b/16/umbraco-forms/developer/healthchecks/images/healthcheck-v14.png differ
diff --git a/16/umbraco-forms/developer/healthchecks/images/healthcheck.png b/16/umbraco-forms/developer/healthchecks/images/healthcheck.png
new file mode 100644
index 00000000000..c701f83f778
Binary files /dev/null and b/16/umbraco-forms/developer/healthchecks/images/healthcheck.png differ
diff --git a/16/umbraco-forms/developer/images/assign-sensitive-data-to-user.png b/16/umbraco-forms/developer/images/assign-sensitive-data-to-user.png
new file mode 100644
index 00000000000..a1cafe90d27
Binary files /dev/null and b/16/umbraco-forms/developer/images/assign-sensitive-data-to-user.png differ
diff --git a/16/umbraco-forms/developer/images/content-app.png b/16/umbraco-forms/developer/images/content-app.png
new file mode 100644
index 00000000000..2ebbfbbf08a
Binary files /dev/null and b/16/umbraco-forms/developer/images/content-app.png differ
diff --git a/16/umbraco-forms/developer/images/exclude-scripts-v9.png b/16/umbraco-forms/developer/images/exclude-scripts-v9.png
new file mode 100644
index 00000000000..d592b5e73d6
Binary files /dev/null and b/16/umbraco-forms/developer/images/exclude-scripts-v9.png differ
diff --git a/16/umbraco-forms/developer/images/exclude-scripts.png b/16/umbraco-forms/developer/images/exclude-scripts.png
new file mode 100644
index 00000000000..571bd847008
Binary files /dev/null and b/16/umbraco-forms/developer/images/exclude-scripts.png differ
diff --git a/16/umbraco-forms/developer/images/form-guid.png b/16/umbraco-forms/developer/images/form-guid.png
new file mode 100644
index 00000000000..71a21c7618c
Binary files /dev/null and b/16/umbraco-forms/developer/images/form-guid.png differ
diff --git a/16/umbraco-forms/developer/images/mark-field-as-sensitive.png b/16/umbraco-forms/developer/images/mark-field-as-sensitive.png
new file mode 100644
index 00000000000..06f2496feab
Binary files /dev/null and b/16/umbraco-forms/developer/images/mark-field-as-sensitive.png differ
diff --git a/16/umbraco-forms/developer/images/select-a-theme.png b/16/umbraco-forms/developer/images/select-a-theme.png
new file mode 100644
index 00000000000..985dd572041
Binary files /dev/null and b/16/umbraco-forms/developer/images/select-a-theme.png differ
diff --git a/16/umbraco-forms/developer/images/sensitive-data-field.png b/16/umbraco-forms/developer/images/sensitive-data-field.png
new file mode 100644
index 00000000000..82243d3840d
Binary files /dev/null and b/16/umbraco-forms/developer/images/sensitive-data-field.png differ
diff --git a/16/umbraco-forms/developer/images/swagger-ui.png b/16/umbraco-forms/developer/images/swagger-ui.png
new file mode 100644
index 00000000000..51358f36b64
Binary files /dev/null and b/16/umbraco-forms/developer/images/swagger-ui.png differ
diff --git a/16/umbraco-forms/developer/images/user-group-permissions.png b/16/umbraco-forms/developer/images/user-group-permissions.png
new file mode 100644
index 00000000000..43e7df5cb94
Binary files /dev/null and b/16/umbraco-forms/developer/images/user-group-permissions.png differ
diff --git a/16/umbraco-forms/developer/images/user-start-folders-v14.png b/16/umbraco-forms/developer/images/user-start-folders-v14.png
new file mode 100644
index 00000000000..d0df3ca2c23
Binary files /dev/null and b/16/umbraco-forms/developer/images/user-start-folders-v14.png differ
diff --git a/16/umbraco-forms/developer/images/user-start-folders.png b/16/umbraco-forms/developer/images/user-start-folders.png
new file mode 100644
index 00000000000..ff01a21c28c
Binary files /dev/null and b/16/umbraco-forms/developer/images/user-start-folders.png differ
diff --git a/16/umbraco-forms/developer/images/validation-pattern.png b/16/umbraco-forms/developer/images/validation-pattern.png
new file mode 100644
index 00000000000..e90ec8642dc
Binary files /dev/null and b/16/umbraco-forms/developer/images/validation-pattern.png differ
diff --git a/16/umbraco-forms/developer/images/wehbook-events-v14.png b/16/umbraco-forms/developer/images/wehbook-events-v14.png
new file mode 100644
index 00000000000..5d737e3c0c9
Binary files /dev/null and b/16/umbraco-forms/developer/images/wehbook-events-v14.png differ
diff --git a/16/umbraco-forms/developer/images/wehbook-events.png b/16/umbraco-forms/developer/images/wehbook-events.png
new file mode 100644
index 00000000000..c8ad67f92a3
Binary files /dev/null and b/16/umbraco-forms/developer/images/wehbook-events.png differ
diff --git a/16/umbraco-forms/developer/iprevaluetextfilestorage.md b/16/umbraco-forms/developer/iprevaluetextfilestorage.md
new file mode 100644
index 00000000000..d25103de140
--- /dev/null
+++ b/16/umbraco-forms/developer/iprevaluetextfilestorage.md
@@ -0,0 +1,69 @@
+# Storing Prevalue Text Files With IPreValueTextFileStorage
+
+Umbraco Forms contains a built-in `Get value from textfile` [Prevalue Source Type](extending/adding-a-prevaluesourcetype.md) that stores the uploaded text file into the physical file system (by default in `umbraco\Data\UmbracoForms\PreValueTextFiles`).
+
+You can replace the default implementation by writing your own `IPreValueTextFileStorage` and registering that using e.g. `builder.Services.AddUnique()` (in `Program.cs` or a composer).
+
+You can also use/inherit from `PreValueTextFileSystemStorage` to change the underlying `IFileSystem` that's used to store the prevalue text files.
+
+## Move files to Media file system
+
+You can use the following composer to move the prevalue text files into the media file system. If the media file system is using Azure Blob Storage, this will remove the files from the local physical file system.
+
+```csharp
+using Umbraco.Cms.Core.Composing;
+using Umbraco.Cms.Core.IO;
+using Umbraco.Cms.Core.Scoping;
+using Umbraco.Forms.Core.Data;
+
+public class PreValueTextFileSystemStorageComposer : IComposer
+{
+ public void Compose(IUmbracoBuilder builder)
+ => builder.Services.AddUnique(factory => new PreValueTextFileSystemStorage(
+ factory.GetRequiredService().FileSystem,
+ factory.GetRequiredService(),
+ "PreValueTextFiles"));
+}
+```
+
+You need to manually move the existing files from `umbraco\Data\UmbracoForms\PreValueTextFiles` to your media storage. The final file path/URL will look like `~/media/PreValueTextFiles/{GUID}/{filename.txt}` and be accessible from the browser.
+
+## Move files to Azure Blob Storage
+
+First, install [Umbraco.StorageProviders.AzureBlob](https://github.com/umbraco/Umbraco.StorageProviders) and configure the Forms storage container, for example by adding the following to your `appsettings.json`:
+
+```json
+{
+ "Umbraco": {
+ "Storage": {
+ "AzureBlob": {
+ "Forms": {
+ "ConnectionString": "UseDevelopmentStorage=true",
+ "ContainerName": "sample-container"
+ }
+ }
+ }
+ }
+}
+```
+
+Next, add the following composer that adds the Forms storage container and stores the prevalue text files into Azure Blob Storage (in `forms/PreValueTextFiles/{GUID}/{filename.txt}`):
+
+```csharp
+using Umbraco.Cms.Core.Composing;
+using Umbraco.Cms.Infrastructure.Scoping;
+using Umbraco.Forms.Core.Data;
+using Umbraco.StorageProviders.AzureBlob.IO;
+
+public class PreValueTextFileSystemStorageComposer : IComposer
+{
+ public void Compose(IUmbracoBuilder builder)
+ => builder.AddAzureBlobFileSystem("Forms", options => options.VirtualPath = "~/forms")
+ .Services.AddUnique(factory => new PreValueTextFileSystemStorage(
+ factory.GetRequiredService().GetFileSystem("Forms"),
+ factory.GetRequiredService(),
+ "PreValueTextFiles"));
+}
+```
+
+You need to manually move the existing files from `umbraco\Data\UmbracoForms\PreValueTextFiles` to your storage container. If you've disabled public access, the stored files are not accessible from the browser.
diff --git a/16/umbraco-forms/developer/localization.md b/16/umbraco-forms/developer/localization.md
new file mode 100644
index 00000000000..ace9fb09a0c
--- /dev/null
+++ b/16/umbraco-forms/developer/localization.md
@@ -0,0 +1,47 @@
+---
+meta.Title: Localization
+---
+
+# Localization
+
+The labels, descriptions, and buttons that make up the backoffice screens for Umbraco Forms can be translated into different languages.
+
+When an editor chooses a language for their account, Umbraco CMS will render appropriate translations. The translations will contain a file for that language and a key for the label in question. If either of these can't be found, the label will be displayed in English (US).
+
+## Language Files
+
+Umbraco Forms ships with translations for the following languages:
+
+ - Czech (`cs-cz.js`)
+ - Danish (`da-dk.js`)
+ - Dutch (`nl-nl.js`)
+ - French (`fr-fr.js`)
+ - Italian (`it-it.js`)
+ - Polish (`pl-pl.js`)
+ - Spanish (`es-es.js`)
+ - UK English (`en-gb.js`)
+ - US English (`en.js`)
+
+If the language you require does not exist, it's possible to create your own by duplicating the default `en.js` file. You can then save it with the appropriate culture code for the language you need and replace the English text with the translated version.
+
+As of Forms 10, the file no longer exists on disk and is shipped as part of the `Umbraco.Forms.StaticAssets` NuGet package. You can open this package, either locally using [Nuget Package Explorer](https://apps.microsoft.com/store/detail/nuget-package-explorer/9WZDNCRDMDM3?hl=en-gb&gl=gb&rtc=1), or [online](https://www.nuget.org/packages/Umbraco.Forms.StaticAssets/) by clicking the "Open in NuGet Package Explorer" link. You'll find the file at `staticwebassets/en.js`.
+
+Once translated, the new file should be saved somewhere in the `App_Plugins` folder for example `App_Plugins/UmbracoFormsLocalization/`. The final step is to register the localization file. This can be done by creating a `umbraco-package.json` like so:
+
+```json
+{
+ "$schema": "../../umbraco-package-schema.json",
+ "name": "Umbraco.Forms.Extensions",
+ "extensions": [
+ {
+ "type": "localization",
+ "alias": "UmbracoForms.Localize.DeDE",
+ "name": " German (Germany)",
+ "meta": {
+ "culture": "de-de"
+ },
+ "js": "/App_Plugins/UmbracoFormsLocalization/de-de.js"
+ }
+ ]
+}
+```
diff --git a/16/umbraco-forms/developer/magic-strings.md b/16/umbraco-forms/developer/magic-strings.md
new file mode 100644
index 00000000000..5e8d9da683f
--- /dev/null
+++ b/16/umbraco-forms/developer/magic-strings.md
@@ -0,0 +1,123 @@
+# Magic Strings
+
+Umbraco Forms has some magic strings that enable you to render values from various sources, such as session, cookies and Umbraco page fields.
+
+## Where can I use magic strings?
+
+Magic strings can be used in form fields as a label, description or default value. As an example they can be used in default values in hidden fields - normally in the form of referral codes from a session, cookie or request item.
+
+These values can also be used for properties and settings in workflows. This means you can use name and email fields from a form to create a personal 'Thank you' email.
+
+## Sources of magic string values
+
+### Request
+
+`[@SomeRequestItem]` this allows you to display an item from the current `HttpContext.Request` with the key of 'SomeRequestItem'.
+
+Some examples of variables that are normally available in `HttpContext.Request`:
+
+* `[@Url]`: Insert the current URL
+* `[@Http_Referer]`: The previous visited URL (if available)
+* `[@Remote_Addr]`: The IP address of the visitor (stored by default by Umbraco)
+* `[@Http_User_Agent]`: The browser of the visitor
+
+The variables are not case-sensitive.
+
+You can use it for any available query string variable in the URL as well. If your URL has the query string `?email=foobar@umbraco.com`, you can get the value of the query string into your field by using `[@email]`.
+
+### Dictionary Items
+
+For multi-lingual websites, rather than hard-coding labels like form field captions, a dictionary key can be entered as, for example, `#MyKey`. When the form is rendered, the placeholder will be replaced by the value of the dictionary item identified by the key, according to the current language.
+
+In most cases, the field must contain only the magic string for the replacement to be carried out. This makes sense for translated values, as you will want the whole phrase replaced when, for example, using one for a field's placeholder.
+
+We also translate dictionary keys found within the rich text field, which will be contained within HTML tags. Here we look for dictionary keys making up the full inner text of a tag. So for example, `#myKey
` would be translated, but `Lorem ipsum #myKey dolor sit amet.
` would not.
+
+### Session & Cookies
+
+`[%SomeSessionOrCookieItem]` this allows you to display an item from the current `HttpContext.Session` with the key of 'SomeSessionOrCookieItem'. The session key can only contain alphanumeric chars and you cannot use dots for example. `[%Member.Firstname]` cannot be used, but `[%MemberFirstname]` can be used. You would have to fill these session keys yourself.
+
+If the item cannot be found in the collection of session keys, it will then try to find the item from the `HttpContext.Cookies` collection with the same key.
+
+### Umbraco Page field
+
+`[#myUmbracoField]` this allows you to insert a property of that page and is based on the alias of the field. If your page has a property with the alias 'title', you can use `[#title]` in your form.
+
+Some extra variables are:
+
+* `[#pageName]`: The nodename of the current page
+* `[#pageID]`: The node ID of the current page
+
+### Recursive Umbraco Page field
+
+`[$myRecursiveItem]` this allows you to parse the Umbraco Document Type property myRecursiveItem. So if the current page does not contain a value for this then it will request it from the parent up until the root or until it finds a value.
+
+### Additional data
+
+When rendering a form, additional data can be provided in the form of a dictionary. As well as being associated with the created record and available within workflows, they can be used for "magic string" replacements.
+
+They are accessed using this syntax: `[+additionalDataKey]`.
+
+### Umbraco Form field
+
+`{myAliasForFormField}` this allows you to display the entered value for that specific field from the form submission. Used in workflows to send an automated email back to the customer based on the email address submitted in the form. The value here needs to be the alias of the field, and not the name of the field.
+
+Some extra variables are:
+
+* `{record.id}`: The ID of the current record - this is only accessible on workflows triggered "on approve" or "on reject" rather than "on submit"
+* `{record.updated}`: The updated date/time of the current record
+* `{record.created}`: The created date/time of the current record
+* `{record.umbracopageid}`: The Umbraco Page ID the form was submitted on
+* `{record.uniqueid}`: The unique ID of the current record
+* `{record.ip}`: The IP address that was used when the form was submitted
+* `{record.memberkey}`: The member key that was used when the form was submitted
+
+### Member properties from a form submission
+
+`{member.FOO}` with the prefix of member, the same syntax will allow you to retrieve information about the submission if it was submitted by a logged-in member.
+
+## Formatting magic strings
+
+Using a magic string such as in the examples above will output the values exactly as read from the source. It's possible to apply a format string to customize the output.
+
+The syntax follows that of AngularJS filters, i.e. `[ | : : ]`.
+
+For example, to truncate a string value read from an Umbraco page field with alias `title`, you would use:
+
+```
+[#title | truncate: 10]
+```
+
+Umbraco Forms ships with the following filters:
+
+| Filter | Function | Arguments | Example |
+| ------------------------------------------------ | ----------------------- | -------------------- | ---------------------------------------------------- |
+| Bound a number | `bound` | min and max bound | `[#field \| bound: 1: 10]` |
+| Convert string to lower case | `lower` | | `[#field \| lower]` |
+| Convert string to upper case | `upper` | | `[#field \| upper]` |
+| Format a number | `number` | format string | `[#field \| number: #0.##%]` |
+| Format a number as a currency | `currency` | | `[#field \| currency]` |
+| Format a date | `date` | format string | `[#field \| date: dd-MM-yyyy HH:mm]` |
+| HTML encode a string | `html` | | `[#field \| html]` |
+| Truncate a string | `truncate` | number of characters | `[#field \| truncate: 10]` |
+
+The format strings used for formatting dates and numbers are the standard or custom .NET [date](https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings) and [numeric](https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings) format strings respectively.
+
+Further magic string format functions can be [created in code](extending/adding-a-magic-string-format-function.md) for use in forms.
+
+## How can I parse these values elsewhere in my C# code or Razor Views?
+
+A service implemented by the `IPlaceholderParsingService` interface is available for use in custom code or views. It's found in the `Umbraco.Forms.Core.Services` namespace.
+
+In a controller you can inject it via the constructor and it can also be injected into views via:
+
+```csharp
+@using Umbraco.Forms.Core.Services;
+@inject IPlaceholderParsingService PlaceholderParsingService
+```
+
+The interface implements a single method, `ParsePlaceHolders`, that can be used for parsing magic strings. There are a few overloads available for use depending on the context.
+
+If parameters for the `Record` or `Form` are omitted, magic strings relating to these objects will be removed.
+
+There is also a public extension method `ParsePlaceHolders()` extending the `string` object in the `Umbraco.Forms.Core.Extensions` namespace, again available with some overloads allowing the provision of a `Form` or `Record` object if available.
diff --git a/16/umbraco-forms/developer/prepping-frontend.md b/16/umbraco-forms/developer/prepping-frontend.md
new file mode 100644
index 00000000000..61217d57cdb
--- /dev/null
+++ b/16/umbraco-forms/developer/prepping-frontend.md
@@ -0,0 +1,80 @@
+# Preparing your Frontend
+
+For Umbraco Forms to work correctly, you need to include some client dependencies.
+
+## Client-Side Validation
+
+Umbraco Forms ships with client-side form validation features provided by the [ASP.NET Client Validation library](https://github.com/haacked/aspnet-client-validation).
+
+You can use the following Razor helper to output script tags containing the dependencies. To access this method you will need a reference to `Umbraco.Forms.Web`:
+
+```csharp
+@using Umbraco.Forms.Web
+
+ @Html.RenderUmbracoFormDependencies(Url)
+
+```
+
+Alternatively, you can add the dependencies to the body tag:
+
+```csharp
+@using Umbraco.Forms.Web
+...
+
+
+ @Html.RenderUmbracoFormDependencies(Url)
+
+```
+
+All dependencies originate from your Umbraco Forms installation, which means that no external references are needed.
+
+If you want to modify the rendering of the scripts, you can provide a object parameter named `htmlAttributes`. The contents of the object will be written out as HTML attributes on the script tags.
+
+You can use this to apply `async` or `defer` attributes. For example:
+
+```csharp
+@Html.RenderUmbracoFormDependencies(Url, new { @async = "async" })
+```
+
+If using `async`, please make sure to [disable the Forms client-side validation framework check](../developer/configuration/README.md#disableclientsidevalidationdependencycheck). This is necessary as it's not possible to guarantee that the asynchronous script will load in time to be recognized by the check. This can then cause a false positive warning.
+
+## Validation Using jQuery
+
+If you want to use jQuery as your validation framework for Umbraco Forms, you can manually add the following client dependencies:
+
+- `jQuery` (JavaScript library)
+- `jQuery validate` (jQuery plugin that provides client-side Form validation)
+- `jQuery validate unobtrusive` (Add-on to jQuery Validation that provides unobtrusive validation via data-* attributes)
+
+You should remove any calls to `@Html.RenderUmbracoFormDependencies(Url)`.
+
+The easiest way to add the dependencies is to fetch them from a [CDN](https://en.wikipedia.org/wiki/Content_delivery_network). There are various CDN services you can use:
+
+- For example: [Microsoft CDN](https://docs.microsoft.com/en-us/aspnet/ajax/cdn/overview).
+- Other CDN services you might want to look at are https://www.jsdelivr.com/ and https://cdnjs.com/about, which may offer better performance and more reliable service.
+
+To add the three client dependencies, see the examples below:
+
+**Example within `head` tags.**
+
+```html
+
+
+
+
+
+```
+
+**Example within `body` tags.**
+
+When adding the script to the bottom of the page, you will also need to render the scripts. For more information, see [Rendering Forms Scripts](rendering-scripts.md) article.
+
+```html
+
+
+
+
+
+
+
+```
diff --git a/16/umbraco-forms/developer/property-editors.md b/16/umbraco-forms/developer/property-editors.md
new file mode 100644
index 00000000000..6533d12e5be
--- /dev/null
+++ b/16/umbraco-forms/developer/property-editors.md
@@ -0,0 +1,39 @@
+# Property Editors
+
+When forms are created, editors will want to add them to pages in Umbraco. To do this they need a Document Type with a property that uses a Data Type based on a Form Picker property editor.
+
+Umbraco Forms provides three variations of a form picker.
+
+Form Pickers
+
+Most commonly used is **Form Picker (single)**. This will allow the editor to select a single form for display on page.
+
+Rarely but feasibly, you will have a requirement to present multiple forms on a page. Should this be appropriate, you can use **Form Picker (multiple)**.
+
+{% hint style="info" %}
+Internally this is used for presenting the list of "Allowed forms" you can select when setting up a form picker datatype.
+{% endhint %}
+
+Finally you can provide further flexibility for the editor to select not only a form but also the theme and redirect as well. For this you will use the **Form Details Picker**.
+
+## Configuring the Data Type
+
+Each property editor allows you to restrict the forms that can be chosen with the Data Type. You do this by setting either or both of the list of "Allowed folders" or "Allowed forms".
+
+Form Picker DataType Configuration
+
+The "Form Details Picker" also allows you to select whether a theme or redirect selection is available.
+
+## Property Value Conversion
+
+The type of a property based on the Form Picker presented in a Razor class library is as follows:
+
+| Option | Description |
+| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
+| **Form Picker (single)** | Single GUID representing the form's identifier. |
+| **Form Picker (multiple)** | Collection of GUIDs representing the form identifiers. |
+| **Form Details Picker** | Instance of the `Umbraco.Forms.Core.PropertyEditors.Models.FormDetails` object, which has properties for the form, theme and redirect. |
+
+## Content Delivery API Expansion
+
+Each reference to a form supports expansion via the Umbraco Content Delivery API, as described [here](./ajaxforms.md#working-with-the-cms-content-delivery-api).
diff --git a/16/umbraco-forms/developer/rendering-forms.md b/16/umbraco-forms/developer/rendering-forms.md
new file mode 100644
index 00000000000..9e5b31ff10f
--- /dev/null
+++ b/16/umbraco-forms/developer/rendering-forms.md
@@ -0,0 +1,84 @@
+---
+description: Learn the different ways of rendering a form on your website when using Umbraco Forms.
+---
+
+# Rendering Forms
+
+There are two options available for rendering a form.
+
+## Rendering Using a View Component
+
+To display a form in your view, you can make a call to a view component. You can use a forms GUID directly or add a form dynamically by referencing a form selected via a Forms Picker.
+
+When selecting a theme, it can be added directly as a string or dynamically by referencing a theme picked via a Theme Picker.
+
+{% tabs %}
+
+{% tab title="Dynamic" %}
+
+```csharp
+@await Component.InvokeAsync("RenderForm", new { formId = @Model.Form,
+ theme = @Model.Theme,
+ includeScripts = false })
+```
+
+This example uses a Forms Picker with `form` as alias, and a Theme Picker with `theme` as alias.
+
+{% endtab %}
+
+{% tab title="Static" %}
+
+```csharp
+@await Component.InvokeAsync("RenderForm", new { formId = Guid.Parse("