-
Notifications
You must be signed in to change notification settings - Fork 1
FlexForms Admin User Manual
A practical guide to the Admin area. Written for tenant administrators who run a FlexForms service for their organisation (for example Local SEND Reform Plans, Transfers, or Visits).
You do not need to be a developer to use this manual. Where a change is made in JSON (templates, some tenant settings), the steps still start from the screens you see. If you have never seen JSON, read JSON in plain English first (about five minutes).
Related guide: Form Template Designer Manual — how to design the JSON schema for a form.
- Who this is for
- How FlexForms is organised
- Open Admin
- Suggested first-week workflow
- Templates: create, version, preview, go live
- Custom status labels
- Dashboard columns (via the template)
- Users and form access
- Roles
- Permissions — how they work
- Organisation settings
-
Event mappings
- 12.1 What this page is for
- 12.2 Events in plain English
- 12.3 Azure Service Bus for people who have never used it
- 12.4 The three layers you must configure
- 12.5 Typed events vs schema events
- 12.6 Recommended order of work
- 12.7 Schema events — what to type on the page
- 12.8 Create the Service Bus topic and subscription
- 12.9 Field mappings — how answers become a message
- 12.10 Mapping JSON reference
- 12.11 Worked examples
- 12.12 Triggers — when the API actually publishes
- 12.13 What the published message looks like
- 12.14 How a downstream system should consume it
- 12.15 Virus scanning (always on)
- 12.16 Who can change this
- 12.17 Event mappings checklist
- 12.18 Troubleshooting event mappings
-
Email placeholder mappings
- 13.1 What this is for
- 13.2 How confirmation emails work
- 13.3 Baseline placeholders (always sent)
- 13.4 Recommended order of work
- 13.5 Step by step — add a custom placeholder
- 13.6 Mapping JSON shape
- 13.7 How to link a placeholder to a form field
- 13.8 Metadata keys you can use
- 13.9 Worked examples
- 13.10 Who can change this
- 13.11 Email placeholders checklist
- 13.12 Troubleshooting email placeholders
- Tenant settings
- Applications (admin list)
- What end users see
- System tools and caches
- Things only a SuperAdmin can do
- Troubleshooting
- Glossary
This manual is for people with the Admin role in a tenant (sometimes called tenant admin).
You can:
- Create and version forms (templates)
- Decide which forms are live for end users
- Give people access to forms
- Create custom roles (for example Caseworker or Template Manager)
- Change wording, banners, and dashboard listing options
- Configure how submissions are published (event mappings)
- Inspect tenant configuration and health
You should not normally change secrets, login providers, or database connection strings. Those belong to the platform team (SuperAdmin). They are listed in Tenant settings so you can recognise them and leave them alone.
| Who | What they can do in Admin |
|---|---|
| Admin (this manual) | Almost everything for this tenant, except creating other tenants and listing all platform tenants |
| SuperAdmin | Everything a tenant Admin can do, plus New tenant, Platform tenants, and assigning the Admin role |
| Template Manager (custom role) | Template tools on the Admin hub (create, version, go live, custom status labels). Not users, roles, or tenant settings |
| User Manager (custom role) | User Manager only. Not Role Manager, Contributor management, or templates |
If you only see some of the cards described below, your account may be a custom role rather than full Admin.
A few words you will see everywhere:
| Term | Meaning |
|---|---|
| Tenant | Your organisation’s isolated copy of the service (hostname, users, forms, settings). Changes you make apply to this tenant only. |
| Template (also form) | The blueprint of a form: questions, task list, validation, dashboard columns. |
| Version | A saved snapshot of that blueprint. Users keep filling the version they started; new applications use the latest schema. |
| Live / Not live | Live forms appear for end users who have access. Not-live forms are drafts you can preview. |
| Application | One person’s (or team’s) filled-in form. Organisation settings can rename this word (for example “reform plan”). |
| Role | A named set of permissions (User, Admin, Caseworker, …). |
| Form access | Which templates a person can open and start. |
Live is not the same as “saved”. Saving a new template version does not publish it to end users. You still Make live on the Admin hub.
- Sign in on your tenant hostname.
- In the service navigation, select Admin.
- You land on the Admin hub (
/admin).

What you see depends on your role. A full tenant Admin typically sees:
| Card | What it is for |
|---|---|
| Tenant templates | List of forms, Live / Not live, Open / Preview, Make live / Make not live |
| Template Management | Create a new template, Template Manager, Choose / preview templates, Custom Status Labels |
| Users & Roles | User Manager (and Role Manager / Contributor management for Admin) |
| Applications | Browse every application for a chosen template |
| Tenant Admin | Organisation settings, Event mappings, Tenant Settings (including email placeholder mappings) |
| System | Clear All Sessions & Caches, plus optional diagnostics |
Tenant Admins (not SuperAdmins) also see a read-only summary: This tenant’s configuration (auth scheme, providers, hostnames).
Use this if you are standing up a new form.
- Create a new template (starts as not live, with a starter schema).
- Open Template Manager, edit the JSON (or paste a designed schema), save a new version.
- Optionally add dashboard columns and contributor settings in that JSON.
- Set custom status labels if you want “In progress” to read as something else.
- Grant to all users or add people individually in User Manager.
- Preview the form from the Admin hub (Not live).
- When ready, Make live.
- Set Organisation settings (wording, banner, filters).
- Create Caseworker / Template Manager roles if colleagues need limited Admin access.
- If another system must receive submit or file-upload data, follow Event mappings (schema, mapping, trigger, then Azure Service Bus topic and subscription).
- If confirmation emails need form answers (for example academy name), follow Email placeholder mappings after the GOV.UK Notify template is ready.
Admin → Template Management → Create a new template
Page: /admin/create-template
Title: Create a template

The page explains:
The new template will belong to this tenant and will not be live until an administrator publishes it.
- Enter a Template name (required, up to 100 characters). Hint example: “School transfer application”.
- Select Create template.
What happens behind the button:
- The template is created for this tenant.
- Version 1.0.0 is added with a small starter form (one task group, one task, one page, one text field).
- You are taken to Template Manager with a success banner Template created, and the add-version form open with a suggested next version 1.0.1.
The starter form is a scaffold. Replace it with your real schema before going live. Contributors are off on the starter (contributorPattern is false) until you enable them in JSON.
This is only on the Admin hub, card Tenant templates. Template Manager does not publish.

For each template you see:
- Name and latest version number
- Status tag: Live (green) or Not live (grey)
- Open (if live) or Preview (if not live)
- Make live or Make not live
Open / Preview selects that template in your session and takes you into the service as that form.
End users only see live forms they have been granted. Admins and Template Managers can still open not-live forms to preview them (an orange preview banner appears).
Admin → Template Management → Template Manager
Page: /admin/template-manager

-
Choose a template to manage — options show
(Live)or(Not live). - Select Open template.
If the tenant has no templates, you see “There are no templates for this tenant” and a button to create one.
- Summary: Template ID, Template Name, Latest Version.
-
Choose a version to view or edit — the latest is marked
(latest). - Select Open version.
- A read-only Schema for version … textarea shows the JSON for that version.
Adding a new version starts from the version you currently have open, not necessarily the latest. The suggested version number still increments from the latest.
-
Select Add New Template Version.
-
New Version Number — for example
1.1,2.0,1.2.1. -
JSON Schema — prefilled from the version you opened. Edit it, or paste a schema produced with the designer manual. If the curly braces and quotes are unfamiliar, start with JSON in plain English.
-
Read the reporting warning (shown in red). Tick:
I agree and confirm I have discussed any changes with the relevant data team
-
Select Save New Version.
If the JSON is invalid, the save is rejected and you should see There is a problem plus the specific messages (missing taskGroups, JSON parse error, and so on). Fix the schema and save again.

After a successful save: Template version created successfully. You are advised to Clear all caches so everyone picks up the new version immediately.
Select Grant to all users and confirm.
This gives every active user in the tenant read and write access to this template. People who already had access are skipped.
Success banner example:
Granted to 12 user(s). 3 already had access. Total tenant users checked: 15.
Use this for a service where everyone in the tenant should be able to start the form. For selective access, use User Manager instead.
Admin → Template Management → Choose / preview templates
Also in the header as Forms when you have more than one form.
Page: /templates
Title: Choose a form

Admins see live and not-live forms (not-live labelled for preview). End users only see live forms they can access.
Select a form, then Go to dashboard.
Admin → Template Management → Custom Status Labels
Page: /admin/custom-status-label-overrides
Title: Admin - Custom Status Overrides

The underlying statuses stay the same (Created, In progress, Submitted, Deleted). You only change the words people see on dashboards, listings, and filters.
- Choose a Template.
- Choose a Base Status.
- Enter the Custom Status Value (the label to show).
- Select Save Overrides.
The cache refreshes immediately. Repeat for each base status you want to rename (for example “In progress” → “Draft”).
There is no Admin screen for dashboard columns. You add them in the template JSON, then save a new version in Template Manager.
The applications dashboard (/applications/dashboard) heading defaults to Your {plural} and can be overridden in Organisation settings, including a {template_name} placeholder that names the form the user is in. The table columns come from the template’s "dashboard" section.

If you omit "dashboard", users see:
- Reference number
- Date started
- Date submitted
- Status
- Action (always kept so people can open a row)
In the root of the template JSON:
"dashboard": {
"columns": [
{ "type": "system", "id": "reference", "order": 10 },
{ "type": "field", "fieldId": "incomingTrustName", "header": "Trust name", "order": 20 },
{ "type": "system", "id": "dateStarted", "order": 30 },
{ "type": "system", "id": "dateSubmitted", "order": 40 },
{ "type": "system", "id": "status", "order": 50 },
{ "type": "system", "id": "action", "order": 60 }
]
}| You set | Meaning |
|---|---|
"type": "system" |
Built-in column. id is one of reference, dateStarted, dateSubmitted, status, action. |
"type": "field" |
Answer from the form. fieldId must match the question’s id. header is the column title. |
order |
Lower numbers appear first. |
Rules you will feel in the UI:
- At most three field columns (any extras are ignored).
- If you only list field columns, the default system columns are still added.
- Column headings come from the latest template version. Cell values come from each application’s own answers. Older applications show a blank cell if that field did not exist yet.
- Prefer simple answers (text, date, radios). File uploads do not work well in a table cell.
You can also point fieldId at a field inside a repeatable collection; several items are shown joined with commas. Full path rules are in the designer manual, §3.1.
After you save the version, Make live (if needed) and clear caches so the dashboard picks up the new headings.
Whether filters appear is not in the template. Turn them on in Organisation settings (Enable application filters).
When enabled, Filter {plural} includes:
- Reference number (all or part)
- Status (uses your custom labels)
- Date started (from / to)
- Date submitted (from / to)
Apply filters / Clear filters. Page size is also set in Organisation settings.
Admin → Users & Roles → User Manager
Page: /admin/user-manager

Lead copy: “Manage who can access forms in this tenant.”
Removing someone does not delete their account. It only clears their access in this tenant.
Each person is a card showing:
- Name, Edit, Permissions
- Role tag (User, Admin, Caseworker, custom roles, …)
- Forms they can access, with Live / Not live (and “Show all forms” if there are more than three)
- Remove from tenant (with a confirmation)
Add new user opens the add screen.
Page: /admin/user-manager/add
Title: Add user

- Name and Email address.
-
Role — the system User role plus any custom roles from Role Manager.
Only a SuperAdmin can assign the tenant Admin role. Tenant Admins do not see Admin in the list. -
Forms — tick the templates this person should use.
Required for the User role. Optional for custom roles that already get application access from Role Manager (for example Caseworker). - Select Add user.
If the email already exists in this tenant, you will be told so.
Page: /admin/user-manager/edit
Title: Edit user
Name and email are read-only. You can change Role and Forms this user can access, then Save.
Changing only form access does not change the person’s role. The access audit trail records that as form access updated, not as a new role assignment.
From a user card, select Permissions.
Page title: Manage permissions

Use this for extra grants on top of the role (for example a one-off template). Each grant is:
- Resource type (Template, Application, ApplicationFiles, …)
-
Resource key (usually a GUID, an email, or
Anywhere allowed) - Access type (Read, Write, Delete — not Manage)
Manage cannot be given to an individual. Create or assign a role that includes Manage instead (Role Manager).
Empty state: “No user-level permissions added yet.”
At the bottom of User Manager: Access audit trail.
“Recent role assignment and access changes in this tenant (last 50).”
Columns: When (UTC), Subject email, Action, Role, Actor email, Details.
Typical actions:
| Action | When you see it |
|---|---|
| RoleAssigned | A user was created or their role changed |
| FormAccessUpdated | Their form (template) list was changed without a role change |
| MembershipDeactivated | They were removed from the tenant |
People who auto-register (first sign-in) do not get every live form:
| Live templates on the tenant | What the new user can open |
|---|---|
| None | Nothing — they stay signed in and see a message to ask an admin for form access |
| Exactly one | That form, automatically |
| More than one | Nothing (same message), unless a default template is configured (see below) |
They must still be able to sign in without a form. After you grant forms in User Manager, they should get access on the next page load (or after signing out and back in).
To auto-assign one form when several are live, a SuperAdmin sets Tenant Settings category SelfRegistration (Target Shared):
{ "DefaultTemplateId": "the-live-template-guid" }Web ExternalApplicationsApiClient:DefaultTemplateId is also used if present. The default must itself be live. Otherwise the user is created with no form access and you pick templates in User Manager.
| Need | Use |
|---|---|
| Everyone in the tenant should use one form | Template Manager → Grant to all users |
| Only some people, or different forms per person | User Manager add/edit |
| A team that can see all applications | Role Manager → Create Caseworker role, then assign it in User Manager |
Invite/remove of extra people on one application is done from that application’s task list when the template has "contributorPattern": true.
Admins and SuperAdmins can also look up who is on an application, or look up a user by email to see the applications they created and who they invited, at Admin → Users & Roles → Contributor management (/admin/contributor-management). Custom User Manager roles cannot open this page.
Admin → Users & Roles → Role Manager
Page: /admin/role-manager
Admin and SuperAdmin only. Custom “user manager” roles cannot open this page.

Copy on the page:
Create custom roles for this tenant and manage the permissions each role grants. System roles cannot be renamed or deleted.
| Role | Purpose |
|---|---|
| User | Standard applicant. Needs at least one form ticked in User Manager unless a custom role already grants application access. |
| Admin | Tenant administrator (this manual). Assigned only by SuperAdmin. |
| SuperAdmin | Platform operator. Not something you assign from tenant User Manager. |
You cannot rename, delete, or edit permissions on system roles.
- Under Create a role, enter a Role name (do not use SuperAdmin or Admin).
- Select Create role.
- Select Manage permissions on that row and add grants (see Permissions).
- Assign the role to people in User Manager.
Create from template adds a custom role with a sensible starter set. You can change permissions afterwards.
| Button | What you get | Typical use |
|---|---|---|
| Create Caseworker role | Read all applications and files in the tenant (Application:Any:Read and ApplicationFiles:Any:Read) |
Staff who list and review everyone’s applications, without becoming Admin |
| Create Template Manager role | Manage templates (Template:Any:Manage) |
Content owners who version forms and go live, without User Manager or Tenant Settings |
Then assign the role on Add user / Edit user.
In Roles in this tenant:
- Custom rows: Rename, Manage permissions, Delete
- Delete asks you to confirm. Users must be moved off the role first.
- System rows: “Permissions are fixed for system roles”
You can ignore this section until you need a custom role. Form checkboxes in User Manager are enough for most applicants.
Every permission looks like:
Resource type + Resource key + Access type
Examples:
| Grant | Meaning |
|---|---|
| Template + (that form’s ID) + Read/Write | Can open and fill that form (what the Forms checkboxes set) |
Template + Any + Manage |
Template Manager tools on the Admin hub |
Application + Any + Read |
See all applications for a template (Caseworker-style Applications nav) |
User + Any + Manage |
User Manager |
Written out they look like Template:Any:Manage. You pick the three parts from dropdowns; you do not have to type the colons.
Role permissions → baseline for everyone with that role
+
User Manager “Forms” → extra template access for that person
+
User Permissions page → extra one-off grants (cannot include Manage)
Admin and SuperAdmin already bypass most of these checks.
Manage (templates or users) must be on a role, not on the user permissions page. That stops a User Manager from quietly making themselves a Template Manager without a named role.
The permissions screens hint this. Any is only valid for certain combinations, including:
- Application — Read
- ApplicationFiles — Read
- Template — Write (start applications on any template)
- Template — Manage (roles only)
- User — Manage (roles only)
- FileValidation — Write (platform file-scanning callback)
For a single form, use that template’s GUID as the resource key (User Manager form ticks do this for you).
Admin → Tenant Admin → Organisation settings
Page: /admin/organisation-settings
This is the safe, form-based way to change how the service looks and reads. Prefer this over editing the same categories as raw JSON in Tenant Settings.

Lead copy: update display terminology, the site-wide notification banner, and dashboard options. You may need to refresh the browser after save to see banner, wording, or dashboard text changes.
Select Save settings at the bottom.
Customise how “application” is labelled across the service (headings, buttons, empty states).
| Field | Hint | Example |
|---|---|---|
| Singular | For example application, reform plan | reform plan |
| Plural | For example applications, reform plans | reform plans |
Effects you will notice:
- Dashboard title Your reform plans (unless overridden in Dashboard text)
- Start a new reform plan (unless overridden in Dashboard text)
- Filter panel Filter reform plans
- Admin applications list still says “applications” in the page title; day-to-day user language follows these terms
Show a GOV.UK notification banner on every page (for example “This is a test environment”).
| Field | Purpose |
|---|---|
| Enabled | Tick to show the banner |
| Heading | For example Important or Warning
|
| Message | The body text |

| Field | Purpose |
|---|---|
| Page size | How many rows per page (1 to 500, default 50) |
| Enable application filters | Shows the filter panel (reference, status, dates) |
| Main heading | Dashboard H1 (for example Your visits). Leave blank to use Your {plural}
|
| In-progress heading | Heading above the list (for example Visits in progress). Leave blank to use {Plural} in progress
|
| Start new heading | Heading for the start section. Leave blank to use Start a new {singular} |
| Start new hint | Supporting text under that heading. Leave blank for the default lead-applicant sentence |
| Start new button text | Primary button label. Leave blank to use Start new {singular} |
Listing options do not change which columns appear. Columns come from the template JSON (section 7).
Any of the five text fields above can include the placeholder {template_name}. It is replaced at page load with the name of the form the user is currently working in, so one setting covers every template in your tenant.
| You type | A user in Transfer sees |
|---|---|
Your applications for {template_name} |
Your applications for Transfer |
{template_name} applications in progress |
Transfer applications in progress |
Start new {template_name} application |
Start new Transfer application |
Notes:
- The placeholder is not case sensitive, so
{TEMPLATE_NAME}works too. - If the service cannot tell which form the user is in, the placeholder is removed and the spacing tidied up, so
Your applications for {template_name}becomes Your applications for. Write your copy so it still reads sensibly without the name. - The name shown is the one on the form in Template Manager. If you rename a form, people already signed in keep seeing the old name until they choose a form again or their session ends.
- Leaving a field blank still falls back to the terminology default (section 11.1); the placeholder only applies to text you enter yourself.
Customise the confirmation page after submit (/application-submitted/{reference}). This category is per template.
| Field | Purpose |
|---|---|
| Template | A form from this tenant’s catalogue, or Default (all templates) as the fallback |
| Confirmation title | Green panel heading (for example Plan submitted). Leave blank to use {Singular} submitted
|
| Page body | Markdown for everything below the reference number |
Markdown uses the same Markdig renderer as form hints. Supported:
-
## Heading/### Subheading - Bullet lists (
- item) and numbered lists (1. item) - Bold and italic
- Links:
[text](https://…)or[email](mailto:name@example.com)
Raw HTML is stripped. Save settings before switching template if you have unsaved changes.
Admin → Tenant Admin → Event mappings
Page: /admin/event-mappings

This chapter is the full guide. You can skip it if your tenant does not send form data to another system. If you do need reporting, data warehouse feeds, or a product-specific processor, read it even if you have never used Azure Service Bus.
High-level design for developers: Event mapping (HLD) in the Web README.
FlexForms stores answers in its own database. Other teams often need a copy of selected answers (or file metadata) at the moment someone submits a form or uploads a file.
This page tells the FlexForms API:
- What shape the outbound message should have (schema event, or a platform “typed” contract).
- Which form fields (and platform facts such as application reference) go into that message (field mapping).
- When to send it (trigger).
Nothing is sent until all three are in place for that form. Saving only a mapping, or only a schema, does not publish.
Configuration is stored in TenantConfig for this tenant only (categories SchemaEvents, EventMappings, EventTriggers, Target Shared so the API can read them). Other tenants cannot see your mappings.
If you have no reporting pipeline, leave the page empty. No triggers configured yet means submit and upload do not publish extra messages. Virus scanning still runs (see 12.15).
| Everyday idea | What FlexForms calls it |
|---|---|
| “Something just happened that another system should hear about.” | An event (a message). |
| “The named moment we care about.” | A trigger: ApplicationSubmitted or FileUploaded. |
| “The label on the message so receivers know which recipe to use.” |
Event type (for example TransferApplicationSubmittedEvent or LsrpPlanSubmitted). |
| “The pigeon-holes in Azure that messages land in.” | A Service Bus topic. |
| “A named inbox hanging off that pigeon-hole.” | A subscription. Your receiving app listens on a subscription, not on the topic itself. |
| “A recipe the platform team already coded in CoreLibs.” | A typed event. |
| “A recipe you invent for this tenant, described with JSON Schema.” | A schema event. |
| “How form answers are copied into the message.” | A field mapping. |
The FlexForms Web app is only the editor. The FlexForms API is the publisher. Your downstream app is the consumer.
Publishing is best-effort. If Service Bus is down or mapping JSON is wrong, the user’s submit or upload still succeeds. Failures are logged on the API. Always test in a non-production environment first.
Azure Service Bus is a Microsoft cloud message broker. Think of it as a post office:
- FlexForms API posts a letter (publish / send).
- The letter goes into a topic (a named pile, for example
lsrp-plan-submitted). - Each interested system has a subscription (its own copy of that pile).
- That system reads letters from its subscription.
You do not create a Service Bus namespace on this Admin page. The platform already points the API at a namespace via MassTransit settings (connection string or managed identity). Ask the platform team for:
- Namespace name (looks like
something.servicebus.windows.net) - Confirmation that the API identity can Send to topics
- Confirmation that your consumer identity can Listen on subscriptions
Topics you usually do not create yourself (platform / virus scan):
| Topic name | Used for |
|---|---|
file-scanner-requests |
API asks ClamAV to scan an upload (ScanRequestedEvent) |
file-scanner-results |
Scanner reports clean or infected (ScanResultEvent) |
Typed product topics (from CoreLibs TopicNames — the Admin catalogue shows the exact name):
| Example event type | Topic name today |
|---|---|
TransferApplicationSubmittedEvent |
transfer-application-submitted |
If the catalogue shows (no topic resolved) for a typed event, MassTransit will not know where to publish it. Raise that with the platform team; they need a matching TopicNames constant in CoreLibs.
Schema event topics are your names. You choose topicName in the schema definition (for example lsrp-plan-submitted). That topic must exist in the same namespace the API uses. Production usually does not auto-create entities.
Naming tips for a new topic:
- Lowercase letters, numbers, hyphens
- Unique in the namespace
- Stable — changing
topicNamelater means creating a new topic and moving consumers
Work from the bottom of the page conceptually, even though the screen lists schema first:
1. Schema event (only if you are not using a platform typed event)
↓
2. Field mapping (template + event type + mapping JSON)
↓
3. Trigger (when to publish + kind Typed or Schema + mapping id)
↓
4. Service Bus (topic exists; consumer has a subscription)
| Layer | TenantConfig category | What it does |
|---|---|---|
| Schema events | SchemaEvents |
Names your message type and which topic to use |
| Field mappings | EventMappings |
Copies form/metadata into properties |
| Triggers | EventTriggers |
Binds a lifecycle moment to an event type |
The Admin page writes those categories for you. You can also inspect them under Tenant settings, but prefer this page.
A mapping does not publish by itself. You must add a trigger that uses the same event type (and the same mappingId you put in the JSON).
At runtime the API finds the mapping by template id + event type. Keep mappingId identical on the mapping and the trigger so operators can tell them apart; do not reuse the same event type with two different mapping ids on one template (only one mapping is stored per template per event type).
| Typed | Schema | |
|---|---|---|
| Who defines the contract | Platform (CoreLibs messaging contracts) | You, on this page |
| Where you see the list | Expand Platform typed-event catalogue | Saved schema events table |
| Topic | From CoreLibs (TopicNames) |
Your topicName
|
| Payload | Deserialised into a C# class |
SchemaEventEnvelope with a dictionary payload
|
| When to use | The event already exists (for example Transfers submit) | Your product is not in CoreLibs yet |
| Name clash | Reserved names | Must not equal a typed event name |
Promote later: when a schema event is stable, the platform can add a typed CoreLibs event. You would then create a typed mapping and trigger and retire the schema one.
Do not create a schema event named ScanRequestedEvent, ScanResultEvent, or any name already in the typed catalogue (for example TransferApplicationSubmittedEvent). The page will reject a clash with a typed name.
- Agree with the receiving team: submit, file upload, or both; which fields they need.
- Decide typed (use catalogue) or schema (invent a name + topic).
- If schema: create the topic and subscription in Azure (12.8), then Save schema event.
- Open Field mappings: choose the template and event type, Load mapping, paste JSON, Save mapping.
- Add a Trigger (
ApplicationSubmittedand/orFileUploaded), kind Typed or Schema, mapping id matching the JSON, Save trigger. - Submit a test application (or upload a test file) in a non-prod tenant.
- Confirm a message appears on the subscription (Azure Portal peek, or your consumer logs).
- Ask the platform team to Refresh tenant configuration if a second API instance looks stale (this page already calls refresh on save).
Schema event type name is the MessageType consumers filter on. Use PascalCase without spaces, for example LsrpPlanSubmitted. This is not the Azure topic name.
Schema definition JSON must be a JSON object with at least:
| Property | Required | Meaning |
|---|---|---|
topicName |
Yes | Exact Azure topic name |
jsonSchema |
Yes | JSON Schema describing payload for humans and consumers. The API does not validate the live payload against this schema at publish time.
|
version |
No | Defaults to 1.0 if omitted. Copied onto the envelope and SchemaVersion header |
description |
No | Shown in the saved table |
Example definition (copy and adapt):
{
"topicName": "lsrp-plan-submitted",
"version": "1.0",
"description": "LSRP plan submitted for reporting",
"jsonSchema": {
"type": "object",
"additionalProperties": false,
"required": [ "applicationReference", "localAuthorityName" ],
"properties": {
"applicationReference": { "type": "string" },
"localAuthorityName": { "type": "string" },
"submittedOn": { "type": "string", "format": "date-time" },
"submittedByEmail": { "type": "string" }
}
}
}Click Save schema event. To change an existing one, Edit definition, then Replace schema event.
jsonSchema is your contract with developers. Keep it in sync with fieldMappings keys.
Do this in Azure Portal (or Bicep/Terraform) on the same namespace the API uses.
- Open the Service Bus namespace.
- Topics → + Topic.
-
Name = the
topicNamevalue (examplelsrp-plan-submitted). Must match character-for-character. - Leave default size/TTL unless the platform team specifies otherwise.
- Create.
For typed events, the topic should already exist (example transfer-application-submitted). Do not invent a different name.
- Open the topic.
- Subscriptions → + Subscription.
- Name it after the consuming app, for example
lsrp-reportingordata-warehouse. - Create.
Each extra consumer needs its own subscription so they do not steal each other’s messages.
| Who | Needs |
|---|---|
| FlexForms API | Send on the topic (namespace-level send is common) |
| Your function / App Service / Logic App |
Listen on the subscription (SAS policy or Azure RBAC Azure Service Bus Data Receiver) |
Connection strings belong in the consumer app settings, not in FlexForms Admin.
Schema messages set header MessageType to your event type name and EventKind to Schema. If several schema events share one topic (not recommended), a subscription filter can be MessageType = 'LsrpPlanSubmitted'. Prefer one topic per schema event.
Typed messages set custom property eventKind = Typed and serviceName = extapi-{TenantName} (tenant name from TenantConfig, not the hostname).
- Template ID — only templates in this tenant’s catalogue.
- Event type — typed catalogue name or a saved schema event name.
- Load mapping — empty editor, or the last saved JSON.
- Edit Mapping JSON.
- Save mapping.
The page may save the mapping under both the API template GUID and the schema’s string templateId (for example form-001) so submit-time lookup works either way.
For typed events, expand Expected properties after load. Extra property names in JSON produce a warning (not a hard error). Missing properties are omitted if the source is empty.
sourceFieldId for form answers must match the template field fieldId (Form Template Designer Manual).
Top-level object:
{
"mappingId": "lsrp-plan-submitted-v1",
"eventType": "LsrpPlanSubmitted",
"description": "Optional note for other admins",
"fieldMappings": {
"propertyNameOnTheEvent": { }
}
}| Field | Rules |
|---|---|
mappingId |
Required. Use the same string on the trigger. |
eventType |
Must match the dropdown (the page fills it if you omit it). |
fieldMappings |
Required, non-empty. Keys = property names on the typed class or keys inside the schema payload. |
Each entry in fieldMappings is a source object:
sourceType |
What it reads | Extra fields |
|---|---|---|
DirectField |
One form answer | sourceFieldId |
ComplexFieldProperty |
Nested value on a complex/autocomplete field |
sourceFieldId, nestedPath (for example ukprn) |
Metadata |
Platform facts, not a question |
sourceFieldId = a metadata key (12.11) |
Static |
Fixed or generated value |
transformationType: currentDateTime / currentDate, or defaultValue
|
Computed |
Several fields combined |
sourceFieldIds, transformationType: concatenate, sum, count, any, checkEquals (needs transformationConfig.compareValue) |
Collection |
Repeating / collection-flow answers |
collectionMapping (see below) |
Optional on any source: defaultValue (used when empty, depending on source type).
Collection (sourceType: Collection):
"academies": {
"sourceType": "Collection",
"collectionMapping": {
"sourceCollectionFieldId": "detailsOfAcademies",
"extractFirst": false,
"itemMappings": {
"ukprn": {
"sourceType": "ComplexFieldProperty",
"sourceFieldId": "trustsSearch-field-flow",
"nestedPath": "ukprn"
}
}
}
}Set extractFirst true and nestedPath to pull a single nested value from the first row instead of an array.
Empty mapped values are skipped (the property is omitted), except where collection mapping returns an empty list.
Trigger: ApplicationSubmitted, kind Schema.
{
"mappingId": "lsrp-plan-submitted-v1",
"eventType": "LsrpPlanSubmitted",
"description": "Reporting feed when a plan is submitted",
"fieldMappings": {
"applicationReference": {
"sourceType": "Metadata",
"sourceFieldId": "applicationReference"
},
"applicationId": {
"sourceType": "Metadata",
"sourceFieldId": "applicationId"
},
"localAuthorityName": {
"sourceType": "DirectField",
"sourceFieldId": "localAuthorityName"
},
"submittedByEmail": {
"sourceType": "Metadata",
"sourceFieldId": "submittedByEmail"
},
"submittedOn": {
"sourceType": "Metadata",
"sourceFieldId": "submittedOn"
}
}
}Use event type TransferApplicationSubmittedEvent (kind Typed). Map only properties that exist on that contract (see Expected properties on the page). Topic is transfer-application-submitted. Your consumer must understand the CoreLibs event class, not SchemaEventEnvelope.
Trigger: FileUploaded. Metadata keys that exist only on this trigger:
sourceFieldId |
Meaning |
|---|---|
fileId |
File GUID |
fileName |
Stored name |
originalFileName |
Name the user uploaded |
filePath |
Storage path without SAS |
fileUri |
Read URI (short-lived SAS in hosted environments; file:// locally). Prefer filePath / fileId if consumers should not receive a download URL |
fileHash |
Content hash |
fileSize |
Bytes |
uploaderUserId |
Uploader GUID |
uploaderEmail |
When known |
uploaderName |
Name of the uploader when known. May be an invited contributor rather than the lead applicant |
leadApplicantName |
Name of the lead applicant — the user who created the application |
uploadedOn |
UTC |
Always available on both triggers: applicationId, applicationReference.
Submit-only metadata: submittedByUserId, submittedByEmail, submittedByFullName, submittedOn.
Example fragment:
"fileName": { "sourceType": "Metadata", "sourceFieldId": "originalFileName" },
"downloadUrl": { "sourceType": "Metadata", "sourceFieldId": "fileUri" },
"schoolName": { "sourceType": "DirectField", "sourceFieldId": "schoolName" }"exportedAt": {
"sourceType": "Static",
"transformationType": "currentDateTime"
},
"fullName": {
"sourceType": "Computed",
"sourceFieldIds": [ "firstName", "lastName" ],
"transformationType": "concatenate"
}| Trigger | Fires when |
|---|---|
ApplicationSubmitted |
The applicant (or an admin completing on their behalf) submits the application |
FileUploaded |
A file is stored on an application (each upload can publish) |
Fields on Save trigger:
| Field | Meaning |
|---|---|
| Trigger |
ApplicationSubmitted or FileUploaded
|
| Event kind |
Typed or Schema — must match how you defined the event |
| Event type | Catalogue or schema name |
| Mapping ID | Same string as mappingId in the JSON |
You can bind more than one event to the same trigger (array). Saving the same event type again replaces that binding.
Remove deletes that trigger + event type pair. It does not delete the mapping or schema.
Wrong kind (Schema trigger for a typed-only name, or Typed for a schema-only name) will fail at publish: typed lookup will not find a CoreLibs class, schema lookup will not find SchemaEvents.
Legacy: old tenants might still have ApplicationSubmission:PublishEvent in Tenant Settings. New work should use Triggers on this page. The API still honours the legacy section for submit until you migrate.
- Body: JSON for the CoreLibs event type.
- MassTransit entity: the CoreLibs topic (example
transfer-application-submitted). - Custom properties:
serviceName=extapi-{TenantName},eventKind=Typed.
Body is a SchemaEventEnvelope:
{
"messageType": "LsrpPlanSubmitted",
"version": "1.0",
"topicName": "lsrp-plan-submitted",
"payload": {
"applicationReference": "LSRP-1001",
"localAuthorityName": "Example Council"
},
"metadata": {
"applicationId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"applicationReference": "LSRP-1001",
"templateId": "11111111-2222-3333-4444-555555555555"
}
}Headers include MessageType, EventKind (Schema), serviceName, TenantId, TenantName, and SchemaVersion when set.
payload is only the keys you mapped. Envelope metadata always includes application id, reference, and template id even if you did not map them.
Typical pattern:
- Azure Function, App Service, or Logic App with a Service Bus subscription trigger.
- Connection to the same namespace.
- If schema: deserialize
SchemaEventEnvelope, readpayload, optionally ignore messages whosemessageTypeis unknown. - If typed: use CoreLibs
Messaging.Contracts(or equivalent JSON) for that event class. - Be idempotent — retries can deliver the same message more than once.
- Do not block the FlexForms user; this is asynchronous.
FlexForms Web does not subscribe to your reporting topics. It only consumes scan results (file-scanner-results) for malware notifications.
Every upload still publishes ScanRequestedEvent to file-scanner-requests. You cannot disable that on this page (ScanRequestedEvent is rejected as a trigger event type). Infected files are handled by the platform scan result pipeline, separate from your reporting events.
Tenant file validation (Excel schema checks and similar) is optional and separate. Infected files are still deleted; a failed validation only marks the file. Setup: 14.6 File validation.
Admin and SuperAdmin of the current tenant. Custom roles (Template Manager, User Manager) do not get this card unless they are also Admin.
After save, the page refreshes tenant configuration so the API picks up changes without a full platform restart. If another API instance still looks old, use Tenant Settings Refresh settings or wait for the provider refresh interval.
- Receiving team agreed fields and trigger (submit and/or upload)
- Typed: topic exists (catalogue Topic column)
- Schema:
topicNamecreated in Azure; subscription created; send/listen rights granted - Schema saved; name does not clash with typed events
- Mapping saved for the correct template and event type
-
mappingIdmatches the trigger - Trigger kind is
TypedorSchemacorrectly - Test submit/upload in non-prod; message peeked on the subscription
- Consumer handles empty omitted properties and duplicate delivery
| What you see | What to check |
|---|---|
| No messages after submit | Is there an ApplicationSubmitted trigger? Mapping for that template + event type? Kind matching typed vs schema? |
| No messages after upload | Need a FileUploaded trigger; submit trigger will not fire on upload |
| Schema publish skipped in API logs |
SchemaEvents missing or topicName empty; trigger kind not Schema
|
| Typed publish skipped | Event type not in CoreLibs catalogue; trigger kind not Typed
|
| Topic not found / unauthorized | Topic name mismatch; wrong namespace; API has no Send |
| Consumer never fires | No subscription; listening to a different topic; competing consumer on the only subscription |
| Empty payload properties |
fieldId typo; metadata key only exists on the other trigger; empty answers are omitted |
| Warning about unknown properties | Typed mapping keys that are not on the C# contract |
| Cannot save schema name | Name collides with a typed event |
| Still using old mapping | Refresh tenant config; clear that you saved under this tenant’s hostname |
API logs (Application Insights) search for Published schema event, Published typed, No EventTriggers configured, Schema event ... is not defined, Event type ... is not a known platform event.
There is no dedicated Admin page for this yet. You configure it under Admin → Tenant Admin → Tenant Settings (category EmailPlaceholderMappings, Target Shared). See 14. Tenant settings.
The field-mapping language is the same as Event mappings (DirectField, ComplexFieldProperty, Collection, Metadata, and so on). If you already map form answers into Service Bus events, you can reuse the same sourceFieldId / nestedPath patterns for emails.
FlexForms sends GOV.UK Notify emails when:
- An application is submitted (confirmation to the applicant)
- A contributor is invited
- A contributor is granted access
The email body lives in Notify (not in FlexForms). FlexForms only supplies:
- Which Notify template ID to use (
EmailTemplates— usually set by the platform team) - A dictionary of personalisation values that fill
((placeholders))in that Notify template
Baseline personalisation (name, reference, dates) is always sent. Email placeholder mappings let you add extra values from the submitted form — for example ((AcademyName)) filled from the academy search field — without a code change.
Configuration is stored in TenantConfig for this tenant only (category EmailPlaceholderMappings, Target Shared so the API can read it).
If you never save this category, emails still work with the baseline keys only.
Applicant submits / contributor invited
│
▼
API loads latest form answers
│
▼
Baseline personalisation (always)
+ optional EmailPlaceholderMappings overlay
│
▼
GOV.UK Notify template → ((placeholders)) filled → inbox
| Piece | Where it lives | Who usually owns it |
|---|---|---|
| Email wording / layout | GOV.UK Notify template | Product / platform with Notify access |
| Notify template GUID per form type | TenantConfig EmailTemplates
|
SuperAdmin / platform |
| Extra placeholders from form answers | TenantConfig EmailPlaceholderMappings
|
Tenant Admin (this section) |
Important: The personalisation key in FlexForms must match the Notify placeholder name exactly (case-sensitive). If Notify has ((AcademyName)), the mapping JSON key must be AcademyName, not academy_name.
These are sent even when EmailPlaceholderMappings is empty. Put the matching ((...)) tokens in your Notify template if you need them.
| Personalisation key | What the applicant sees |
|---|---|
user_full_name |
Submitter’s full name |
application_reference |
Human-readable application reference |
submitted_date |
Date as dd/MM/yyyy
|
submitted_time |
Time as HH:mm
|
| Personalisation key | What the invitee sees |
|---|---|
contributor_name |
Contributor’s name |
application_reference |
Application reference |
added_date |
Date as dd/MM/yyyy
|
added_time |
Time as HH:mm
|
| Personalisation key | What the contributor sees |
|---|---|
contributor_name |
Contributor’s name |
application_reference |
Application reference |
granted_date |
Date as dd/MM/yyyy
|
granted_time |
Time as HH:mm
|
access_types |
Comma-separated access types (for example Read, Write) |
Notify template ID resolution for access-granted emails still uses the ContributorInvited Notify template entry today. Personalisation mappings use the separate email type key ContributorAccessGranted so you can send different extra placeholders without changing that shared Notify ID.
- Decide which extra facts from the form should appear in the email (for example academy name, trust name).
- In GOV.UK Notify, edit the template and add
((YourPlaceholderName))where the text should appear. Save and note the exact spelling. - In the form template schema, find the question’s
fieldId(see Form Template Designer Manual). For nested values (autocomplete / complex objects), note the property path (for examplenameorukprn). - In FlexForms Tenant Settings, add or update
EmailPlaceholderMappings(Target Shared) with a mapping for the right template and email type (13.5). - Submit a test application (or invite a contributor) in a non-production environment and check the email in Notify’s test inbox / letterbox.
Example goal: show the academy name in the application submitted confirmation email.
In the Notify template body, add something like:
You submitted an application for ((AcademyName)).
Your reference is ((application_reference)).
Keep the existing baseline tokens (application_reference, user_full_name, and so on) if you still need them.
Open the live template schema (Template Manager / Forms designer). Find the academy question. Note its fieldId — for example academiesSearch. If the answer is a JSON object with a display name, the nested property is usually name.
- Open Admin → Tenant Admin → Tenant Settings.
-
Add a setting (or Update if
EmailPlaceholderMappingsalready exists). -
Category:
EmailPlaceholderMappings -
Target:
Shared(required — the API must read this at send time) - Settings JSON: use the shape in 13.6. Example for one form:
{
"form-001": {
"ApplicationSubmitted": {
"mappingId": "transfer-submitted-email-v1",
"eventType": "ApplicationSubmitted",
"description": "Extra personalisation for Transfers submitted email",
"fieldMappings": {
"AcademyName": {
"sourceType": "ComplexFieldProperty",
"sourceFieldId": "academiesSearch",
"nestedPath": "name"
}
}
}
}
}- Validate / diff, then Add setting or Update.
- If another admin changed config recently, Refresh settings first.
Use either:
- The schema string
templateId(for exampleform-001), or - The API template GUID
The API looks up the exact key first, then falls back across sibling template keys for the same email type (same behaviour as Event mappings). Prefer saving under the key your team already uses for Event mappings so both stay aligned.
You may nest several email types under the same template key:
{
"form-001": {
"ApplicationSubmitted": { "...": "..." },
"ContributorInvited": { "...": "..." },
"ContributorAccessGranted": { "...": "..." }
}
}Outer object: template key → email type → mapping.
Each mapping object:
{
"mappingId": "my-email-mapping-v1",
"eventType": "ApplicationSubmitted",
"description": "Optional note for other admins",
"fieldMappings": {
"NotifyPlaceholderName": { }
}
}| Field | Rules |
|---|---|
mappingId |
Required. A label for this version of the mapping (for your records). |
eventType |
Required. Must match the email type key: ApplicationSubmitted, ContributorInvited, or ContributorAccessGranted. |
fieldMappings |
Required. Keys = Notify personalisation names. Values = how to fill them (same DSL as Event mappings). |
Merge behaviour
- Baseline keys are always included.
- Mapped keys are added on top.
- If you map a key that already exists in the baseline (for example
user_full_name), the mapped value wins. - Empty mapped values are skipped (that placeholder is not overwritten / not added).
Each entry under fieldMappings is a source object. sourceFieldId for form answers must match the template field fieldId.
sourceType |
What it reads | Extra fields |
|---|---|---|
DirectField |
One form answer (plain text / simple value) | sourceFieldId |
ComplexFieldProperty |
Nested value on a complex / autocomplete field |
sourceFieldId, nestedPath (for example name, ukprn) |
Collection |
First row or mapped rows from a repeating collection |
collectionMapping (same as Event mappings — see 12.10) |
Metadata |
Platform facts (not a question on the form) |
sourceFieldId = a metadata key (13.8) |
Static |
Fixed or generated value |
transformationType: currentDateTime / currentDate, or defaultValue
|
Computed |
Several fields combined |
sourceFieldIds, transformationType: concatenate, sum, count, any, checkEquals
|
Optional on any source: defaultValue.
Notify tip: Prefer string-friendly values (names, references, short text). Large JSON blobs are not useful in an email body.
Use "sourceType": "Metadata" and set sourceFieldId to one of these.
Always available (any of the three email types)
sourceFieldId |
Meaning |
|---|---|
applicationId |
Application GUID |
applicationReference |
Human-readable reference |
Application submitted only
sourceFieldId |
Meaning |
|---|---|
submittedByUserId |
Submitter’s user id |
submittedByEmail |
Submitter’s email |
submittedByFullName |
Submitter’s full name |
submittedOn |
UTC timestamp of submit (raw; baseline already formats date/time separately) |
Contributor invited only
sourceFieldId |
Meaning |
|---|---|
contributorName |
Contributor display name |
contributorEmail |
Contributor email |
addedOn |
When they were added |
Contributor access granted only
sourceFieldId |
Meaning |
|---|---|
contributorName |
Contributor display name |
contributorEmail |
Contributor email |
grantedOn |
When access was granted |
accessTypes |
Access types string |
{
"9A4E9C58-9135-468C-B154-7B966F7ACFB7": {
"ApplicationSubmitted": {
"mappingId": "transfer-submitted-email-v1",
"eventType": "ApplicationSubmitted",
"fieldMappings": {
"AcademyName": {
"sourceType": "ComplexFieldProperty",
"sourceFieldId": "academiesSearch",
"nestedPath": "name"
}
}
}
}
}Notify body: Thank you for submitting an application about ((AcademyName)).
{
"form-001": {
"ApplicationSubmitted": {
"mappingId": "transfer-submitted-email-v2",
"eventType": "ApplicationSubmitted",
"fieldMappings": {
"OutgoingTrustName": {
"sourceType": "Collection",
"collectionMapping": {
"sourceCollectionFieldId": "detailsOfOutgoingTrusts",
"extractFirst": true,
"nestedPath": "trustsSearch-field-flow.name"
}
}
}
}
}
}{
"form-001": {
"ApplicationSubmitted": {
"mappingId": "plan-submitted-email-v1",
"eventType": "ApplicationSubmitted",
"fieldMappings": {
"LocalAuthorityName": {
"sourceType": "DirectField",
"sourceFieldId": "localAuthorityName"
},
"user_full_name": {
"sourceType": "Metadata",
"sourceFieldId": "submittedByFullName"
}
}
}
}
}{
"form-001": {
"ContributorInvited": {
"mappingId": "transfer-contributor-invite-email-v1",
"eventType": "ContributorInvited",
"fieldMappings": {
"AcademyName": {
"sourceType": "ComplexFieldProperty",
"sourceFieldId": "academiesSearch",
"nestedPath": "name"
}
}
}
}
}The API loads the application’s latest saved answers when building contributor emails, so form-based placeholders work there too (as long as those answers were saved before the invite).
| Who | What they can do |
|---|---|
| Tenant Admin | Add / update EmailPlaceholderMappings via Tenant Settings (safe category) |
| SuperAdmin | Same, plus EmailTemplates (Notify template GUIDs) and provider secrets under Email
|
| Product / Notify editors | Change the Notify template wording and ((placeholders))
|
You need Notify access (or a colleague who has it) to add new ((...)) tokens. Saving FlexForms config alone does not change the email layout.
- Notify template updated with the new
((PlaceholderName))spelling - Baseline tokens still present if you still need name / reference / dates
-
fieldIdcopied from the form schema (spelling and casing match) - For complex fields,
nestedPathmatches the stored property (oftenname) -
EmailPlaceholderMappingssaved with Target Shared - Outer key is the correct template (GUID or schema
templateId) - Inner key is the correct email type (
ApplicationSubmitted/ContributorInvited/ContributorAccessGranted) -
eventTypeinside the mapping matches that email type - Tested in non-prod; personalisation visible in Notify preview / received email
| What you see | What to check |
|---|---|
| Email arrives but custom text is blank | Placeholder name mismatch Notify vs JSON key; empty form answer (empty values are skipped); wrong fieldId / nestedPath
|
| Email never includes the new placeholder | Mapping saved under wrong template key or wrong email type; Target not Shared; tenant config not refreshed |
| Only baseline fields appear | No EmailPlaceholderMappings for this template + email type (that is normal until you add one) |
| Contributor email missing form values | Answers not saved yet on the application; wrong collection / field Ids |
| Notify rejects the send | Notify template does not define that personalisation key, or value type is unexpected — keep values as short strings |
| Wrong Notify template altogether | That is EmailTemplates / host mapping — ask SuperAdmin; not fixed by placeholder mappings |
API logs (Application Insights) search for Applying email placeholder mapping, No EmailPlaceholderMappings, Email sent successfully, Could not resolve email template.
Admin → Tenant Admin → Tenant Settings
Page: /admin/tenant-settings

This is the full configuration editor: categories of JSON stored for Web, API, or Shared. Organisation settings and Event mappings write into the same store through friendlier screens. Use Tenant Settings when you need a category that has no dedicated page (for example Email placeholder mappings), or when you are asked to by the platform team.
Secret values are shown decrypted here and encrypted again when Secret is ticked on save.
Select Refresh settings after another admin has changed config, or if health looks stale.
A table of checks (Pass / Warn / Fail), including:
- Config source
- Settings loaded
- Hostname mapping
- CORS origins
- Interactive auth scheme
- Catalogue refresh
Plus a read-only snapshot of effective runtime configuration.
Treat a Fail as “ask the platform team” unless you just changed a setting and can Validate / diff it.
Prefer the dedicated screens first. If you must use this page, these categories match everyday product choices:
| Category | Target | What it controls | Better UI |
|---|---|---|---|
| ApplicationTerminology | Web | Singular / plural labels | Organisation settings |
| NotificationBanner | Web | Site-wide banner | Organisation settings |
| Dashboard | Web | Page size, filters, and dashboard display text | Organisation settings |
| ApplicationSubmittedPage | Web | Per-template confirmation title and markdown body | Organisation settings |
| EventMappings | Shared | Field mappings | Event mappings |
| SchemaEvents | Shared | Tenant event shapes | Event mappings |
| EventTriggers | Shared | Submit / upload publish bindings | Event mappings |
| EmailPlaceholderMappings | Shared | Extra GOV.UK Notify personalisation from form answers | This page (JSON) — see Email placeholder mappings |
| Layout | Web | Service name in the header, phase banner text and links | This page (JSON) — there is no separate form |
| FileValidation | Shared | Whether submit is blocked until a tenant function validates eligible files | This page (JSON) — see 14.6 |
| FormEngine | Web | Search APIs behind autocomplete questions (endpoint, API key or client credentials, dropdown/confirmation labels) | This page (JSON) — see 14.7 |
Layout example (illustrative):
{
"ServiceName": "Local SEND Reform Plans",
"PhaseBanner": {
"PhaseText": "Beta",
"Links": [
{ "Text": "Feedback", "Url": "/feedback" }
]
}
}ServiceName is the product name in the GOV.UK header and the browser title on the dashboard.
Do not edit these unless you have been briefed. They can lock people out or break the API.
| Category | Why it is sensitive |
|---|---|
| Authentication | Forces the login scheme (TestAuthentication, DfESignIn, EntraSso) |
| TestAuthentication | Dev/test login (often secret) |
| EntraSso / DfESignIn | Real SSO client IDs, secrets, endpoints |
| Authorization | API token behaviour |
| ConnectionStrings | Databases |
| InternalServiceAuth | Machine-to-machine keys |
| AuthProviders | API keys / mTLS. Needed for the file-validation callback — see 14.6 |
| AllowedHosts | Which hostnames the app accepts |
| FeatureManagement | Feature flags |
| Email / EmailTemplates | Notify API key and template GUIDs (platform-owned) |
| FormEngine | Contains search API keys or OAuth client secrets. You may add a ComplexFields entry when briefed — see 14.7. Do not delete existing Trust / Establishment / Upload ids. |
The page also explains how SuperAdmins switch login without a platform restart (TestAuthentication / EntraSso / Authentication Scheme). Tenant Admins should not do this unprompted.
Existing rows show Category, Target, JSON, Secret, then:
- Show value / Hide value (secrets)
- Validate / diff — checks the JSON before save
- Update
- Delete (cannot be undone)
Add a setting:
- Category — use a name from the cookbook list on the page.
-
Target —
Shared,Api, orWeb(must match what that category supports). - Settings JSON
- Tick Secret (encrypt at rest) if the payload contains passwords or keys.
- Validate / diff, then Add setting.
Export settings / Import settings copy configuration between environments. Import skips secret placeholders. Use with care.
Audit log on this page lists recent setting changes: When (UTC), Action, Category, Target, Actor.
This is optional. It is not virus scanning (that is always on — see 12.15).
Use it when your organisation has an Azure Function that checks uploaded files (for example Excel columns) and FlexForms should block submit until that check passes. A failed check keeps the file and marks it Failed. Malware still deletes the file.
You need two Tenant Settings rows, plus a FileUploaded event so the function is told about each upload.
This category is not a secret. Add or update it on this page:
{
"DefaultMode": "Off",
"Extensions": [ ".xlsx", ".xls" ],
"Templates": {
"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee": "RequirePassed"
}
}Replace the GUID with your form’s template Id (Template Manager / Diagnostics).
| Mode | What applicants experience |
|---|---|
Off |
Ignore validation (default). Upload status stays —. |
FailOnInvalid |
Submit is blocked only after the function reports the file as invalid. |
RequirePassed |
Eligible files must be validated successfully. Pending also blocks submit. |
Extensions is optional. Empty or omitted means every upload is eligible when mode is not Off. With [".xlsx"], photos stay NotRequired and never block submit.
RequirePassed can leave people stuck if the function is down. Prefer FailOnInvalid until the function is reliable.
The function must call:
POST /v1/integrations/files/{fileId}/validation-result
with headers X-Tenant-ID (your tenant GUID) and X-Api-Key (the raw secret). Signing in as Admin does not authorise this call.
FlexForms stores only a hash of the key. Treat the raw key like a password: give it to the function configuration, never paste it into Tenant Settings.
- Generate a raw key in PowerShell:
[guid]::NewGuid().ToString("N")Copy that value into the function’s X-Api-Key setting.
- Hash the same string (Windows PowerShell 5.1):
[BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash([Text.Encoding]::UTF8.GetBytes("paste-raw-key-here"))).Replace("-","").ToLower()- On Tenant Settings, add or update
AuthProviders. Tick Secret (encrypt at rest). Target Api or Shared. JSON:
{
"Providers": [
{
"Name": "file-validation",
"Kind": "ApiKey",
"IsServicePrincipal": true,
"KeyHash": "<paste-the-hash-from-step-2>",
"Roles": ["FileValidation"]
}
]
}Leave "IsServicePrincipal": true. If it is missing or false, the function gets 403 even with a correct key.
- Validate / diff, then Add setting or Update. Select Refresh settings.
Do not put this key in InternalServiceAuth. That category is for a different machine login.
If AuthProviders already has other providers, add this object to the Providers array — do not replace the whole list unless you intend to.
Under Event mappings, bind File uploaded to the event your function already consumes (same pattern as 12.12 Triggers). The callback must not publish onto FlexForms Service Bus; it only HTTP-posts the result.
| Surface | Behaviour |
|---|---|
| Upload Status column | Validation pending / Validated / Validation failed |
| Preview submit | Disabled when any blocking file remains; names are listed |
| Banner and Notifications | Live update when the function posts a result |
Technical contract: flexforms-api README — File validation.
-
FileValidationTarget Shared, mode set on the right template GUID -
Extensionsmatches the files you actually want checked - Raw API key only in the function; hash in
AuthProviders -
IsServicePrincipalistrueandRolesincludesFileValidation - Secret ticked on
AuthProviders; Refresh settings after save -
FileUploadedtrigger publishes to the function - Non-prod test: upload → pending → function → status + submit gate
Search questions (trusts, academies, members of parliament, and similar) are not fully defined in the form template. The template only names a config id. The live URL, secrets, and most display options live in Tenant Settings category FormEngine (Target Web).
Applicants never talk to the third-party API. They type into FlexForms; FlexForms looks up the config for that id and calls the API on the server.
Related: Form Template Designer Manual — complexField.
Template field type "complexField"
└── complexField.id (e.g. TrustComplexField)
Tenant Settings FormEngine (Target Web)
└── ComplexFields[] matching that Id
├── ApiEndpoint
├── Auth (ApiKey or ClientCredentials)
└── optional DropdownDisplay / ConfirmationDisplay
You cannot invent a new complexField.id in the template alone. Add the id here first (or ask platform), then reference it in Template Manager.
Tick Secret (encrypt at rest) on the FormEngine row whenever it contains an API key or client secret. After save, select Refresh settings.
- Open Admin → Tenant Admin → Tenant Settings.
- Find FormEngine with Target Web, or Add a setting with Category
FormEngine, Target Web. - Put a
ComplexFieldsarray in the JSON (keep any existing entries; add a new object rather than deleting trusts/academies). - Tick Secret.
- Validate / diff, then Add setting or Update.
- Refresh settings.
- In Template Manager, add a field with
"type": "complexField"and"complexField": { "id": "<same Id>" }. Save a new version and preview.
Use this when the third-party API expects a static key in an ApiKey HTTP header (the Academies / Trusts pattern).
{
"ComplexFields": [
{
"Id": "TrustComplexField",
"FieldType": "autocomplete",
"ApiEndpoint": "https://example.test/v4/trusts?search={0}",
"ApiKey": "your-api-key",
"MinLength": 3,
"Placeholder": "Start typing to search for Trusts...",
"Label": "Trust"
}
]
}| Property | Required | What it does |
|---|---|---|
Id |
Yes | Must match complexField.id in the template. PascalCase, unique in this list. |
FieldType |
Yes for search |
autocomplete. Use upload only for file-upload complex fields. |
ApiEndpoint |
Yes | HTTP GET URL. Put {0} where the user’s search text should go. If there is no {0}, FlexForms appends ?q= (or &q= if a query string already exists). |
ApiKey |
For this auth type | Sent as header ApiKey. |
MinLength |
No | Characters before search runs (default 3). |
Placeholder |
No | Grey text in the search box. |
AllowMultiple |
No |
true to let the user pick more than one result. |
MaxSelections |
No |
0 means no limit when multiple is allowed. |
Label |
No | Used in “Is this the right {label}?” and collection item labels. Default Item. |
If one complex field has no ApiKey, FlexForms may reuse another field’s key in the same list, or FormEngine:AcademiesApiKey. Do not rely on that for a new API — set ApiKey on the field that needs it.
Use this when the third-party API does not take an API key. FlexForms first POSTs to a token URL (grant_type=client_credentials), then calls your search URL with Authorization: Bearer …. Tokens are cached until just before they expire; a 401 retries once with a fresh token.
{
"ComplexFields": [
{
"Id": "MemberComplexField",
"FieldType": "autocomplete",
"ApiEndpoint": "https://members.example/search?q={0}",
"AuthType": "ClientCredentials",
"TokenEndpoint": "https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token",
"ClientId": "your-app-id",
"ClientSecret": "your-client-secret",
"Scope": "api://your-api/.default",
"MinLength": 3,
"Placeholder": "Start typing a name...",
"Label": "Member"
}
]
}| Property | Required | What it does |
|---|---|---|
AuthType |
Recommended |
ClientCredentials. If you omit it, FlexForms still uses client credentials when TokenEndpoint, ClientId, and ClientSecret are all set. Set ApiKey (the auth type) only when you must force the API-key header. |
TokenEndpoint |
Yes | Token URL. FlexForms POSTs application/x-www-form-urlencoded with grant_type, client_id, client_secret, and scope when present. |
ClientId |
Yes | Confidential client id. |
ClientSecret |
Yes | Confidential client secret. Always store this category as Secret. |
Scope |
Usually | Many Entra APIs need api://{app-id}/.default. Omit only if the token endpoint does not use scope. |
Do not put ApiKey on a client-credentials field. The two auth types are alternatives, not combined.
Certificate / managed-identity login for the search API is not supported here. If the provider only offers those, talk to the platform team.
| Endpoint pattern | What FlexForms does |
|---|---|
https://api.example/search?q={0} |
Replaces {0} with the encoded query. |
https://api.example/search/{0} |
Same, in the path. |
https://api.example/search (no {0}) |
Calls https://api.example/search?q=…. |
| Extra query flags | You may add them on the URL. IncludeOnlyAcademies is stripped before the HTTP call and used only for establishment filtering. |
The search response must be a JSON array, or an object with data, results, items, or values as an array.
FlexForms copies string, number, and boolean properties from each result (and a nested name where present). Arrays such as roles are ignored. A display name is still set from the first of: name, title, label, value, displayName, groupName, text.
By default (no extra properties):
| Surface | Built-in behaviour |
|---|---|
| Dropdown row | The result name, plus UKPRN, code, and/or Companies House number when those properties exist |
| “Is this the right …?” after Search | Trusts/academies-style fields (trustname, UKPRN, postcode, and so on) depending on the config id |
| Check your answers | Bold name, then postcode, UKPRN, and Companies House number when present |
To change that without code, add display expressions. The template can override the same keys; if neither is set, the table above still applies.
Expression syntax (same in Tenant Settings and in the template):
- Concatenate JSON properties and quoted text:
displayName + " - " + constituencyName - Or placeholders:
{firstName} {lastName} - Property names must match the JSON from the API (
displayName,firstName,constituencyName, …). - Empty properties are skipped (you do not get a trailing
-).
Worked example — members search:
{
"Id": "MemberComplexField",
"FieldType": "autocomplete",
"ApiEndpoint": "https://members.example/search?q={0}",
"AuthType": "ClientCredentials",
"TokenEndpoint": "https://login.example/oauth2/v2.0/token",
"ClientId": "...",
"ClientSecret": "...",
"Scope": "api://members/.default",
"DropdownDisplay": "displayName + \" - \" + constituencyName",
"ConfirmationDisplay": "firstName + \" \" + lastName",
"Label": "Member"
}| Property | Where the user sees it |
|---|---|
DropdownDisplay |
Each row in the search list, and the text left in the box after they choose |
ConfirmationDisplay |
Check your answers / preview. Also drives which JSON properties are listed on “Is this the right …?” |
The stored answer is still the full JSON object (id, names, email, constituency, and so on), not only the label. Event mappings and email placeholders can still use ComplexFieldProperty with nestedPath such as email or constituencyName.
If both the template and this config set DropdownDisplay / ConfirmationDisplay, the template wins.
| Role | Typical work |
|---|---|
| Tenant Admin | Add or update FormEngine on Web (with platform-supplied URLs and secrets) |
| SuperAdmin / platform | Issue API keys or Entra app registrations; confirm the token URL and scope |
| Template Manager | Point a question at the Id; optional display expressions on complexField
|
-
FormEngineTarget Web; Secret ticked if keys or secrets are present - New object added to
ComplexFields(existing Trust / Establishment / Upload ids kept) -
Idmatches the templatecomplexField.idexactly (case-insensitive, but keep PascalCase) -
FieldTypeisautocomplete -
ApiEndpointreachable from the Web app (not only your laptop) - Either
ApiKeyorTokenEndpoint+ClientId+ClientSecret(not a mix) - Refresh settings after save
- Preview the form: type past
MinLength, pick a result, check the dropdown label and check your answers - If mapping emails or events, confirm nested property names on the stored JSON
| What you see | Likely cause |
|---|---|
| Empty dropdown | Query shorter than MinLength; no ApiEndpoint; API returned an error; results had no name / displayName and no DropdownDisplay that resolved |
| HTTP 401 / 403 in Web logs | Wrong ApiKey, or token request failed (TokenEndpoint / ClientId / ClientSecret / Scope) |
| “No API endpoint configured” in logs |
Id in the template does not match any ComplexFields entry after refresh |
| Dropdown shows a name but confirmation is blank |
ConfirmationDisplay uses property names that are not on the stored object; omit it to use the built-in name layout |
| Old trust search still works, new search does not | You replaced the whole ComplexFields array instead of appending; restore the previous ids |
| Changes not visible | Refresh settings, then hard-refresh the form (or clear sessions/caches) |
Admin → Applications → View applications
Page: /admin/applications
Admin and SuperAdmin only (not Caseworker).

Choose a Template to list every application for that form in this tenant, newest first. Columns: Reference, Application ID, Date created, Open (new tab).
This is a catalogue for support. Caseworkers use the main Applications item in the header instead, which uses the same filters as the dashboard when filters are enabled.
After you make a form live and grant access, a typical applicant gets:
| In the header | When |
|---|---|
| Service name (links to Your {plural}) | Always when signed in |
| Forms | Only if they have more than one live form they can access |
| Notifications | Always |
| Log out | Always |
| Admin | Not shown |
| Applications | Only with Caseworker-style Application:Any:Read, or Admin |

On the dashboard they can Start a new {singular}, continue in-progress work, and open submitted items. Status text follows your custom labels. Extra columns follow the template dashboard section.
If they have exactly one live form, they may skip Choose a form and go straight to the dashboard.
Previewing a not-live form as an admin shows: THIS IS A PREVIEW OF {template name}.
Admin hub → System → Clear All Sessions & Caches
Use this after you publish a new template version if people still see the old questions or dashboard headings.
You may need to choose the form again afterwards (Forms or Admin → Open / Preview).
Diagnostics (when a template is selected) shows the active template name, id, version, cache key, and session tokens. Useful when raising a support ticket; not needed day to day.
Template Manager’s success message also offers Clear all caches after a new version.
You will not see these as a tenant Admin (by design):
| Screen | Purpose |
|---|---|
New tenant (/admin/duplicate-tenant) |
Clone a tenant, including a new service name |
Platform tenants (/admin/platform-tenants) |
List every tenant on the platform |
| Assigning the Admin role in User Manager | Only SuperAdmins get Admin in the role dropdown |
If you need a second tenant administrator, ask a SuperAdmin to assign the Admin role.
| What you see | What to try |
|---|---|
| No Admin link | You are not Admin, SuperAdmin, Template Manager, or User Manager. Ask a tenant Admin to grant a role. |
| Admin hub missing Users & Roles | You likely have Template Manager only. |
| Admin hub missing templates | You likely have User Manager only. |
| No Contributor management button | Expected unless you are Admin or SuperAdmin. User:Any:Manage is User Manager only. |
| “You do not have permission” on Platform tenants | Expected for tenant Admins. That button is SuperAdmin-only. |
| Saved a template version but users still see the old form | Make live if it is still Not live; Clear All Sessions & Caches; users may need to refresh or pick the form again. |
| Invalid JSON save with no explanation | You should now see an error summary and messages under JSON Schema. If not, hard-refresh the Template Manager page. |
| Banner or terminology not updating | Organisation settings: refresh the browser. Banner only shows when Enabled is ticked and Message is not empty. |
| User has a role but cannot open a form | User role needs Forms ticked, unless the role already grants application access (for example Caseworker). |
| Caseworker sees Applications in the nav but cannot list | They need Application:Any:Read (Create Caseworker role) and usually ApplicationFiles:Any:Read for files. They may also need template read for custom statuses. |
| Dashboard missing a custom column | Check fieldId matches the question, you have no more than three field columns, and you saved a new template version. Older applications can show blank cells. |
| Grant to all users succeeded but the banner showed zeros | Fixed in a recent release; counts should match people actually updated. |
| Submit succeeded but reporting never received a message | See 12.18 Troubleshooting event mappings. Check triggers, mapping, Service Bus topic/subscription, and API logs. |
| Confirmation email missing academy name / custom text | See 13.12 Troubleshooting email placeholders. Check Notify ((placeholder)) spelling, EmailPlaceholderMappings (Target Shared), and form fieldId. |
| Upload stays “Validation pending” / submit stays blocked | See 14.6. Check FileValidation mode, FileUploaded trigger, function X-Api-Key (raw) vs AuthProviders KeyHash, IsServicePrincipal: true, and Refresh settings. |
| Autocomplete search empty / 401 / wrong labels | See 14.7. Check FormEngine Target Web, matching Id, Refresh settings, API key vs client credentials, and DropdownDisplay / ConfirmationDisplay. |
| Term | Plain meaning |
|---|---|
| Application / reform plan / … | One submitted or in-progress form. The on-screen word is configurable. |
| Catalogue | The templates that belong to this tenant (owned or mapped). |
| Claim / grant | A single permission assigned to a role or user. |
| Contributor | Someone invited onto a specific application, not necessarily a tenant user with their own forms. |
| Live | Visible to end users who have access. |
| JSON | Structured text made of labelled facts (keys and values). Templates and some tenant settings are stored this way. See JSON in plain English. |
| Schema / JSON schema | The template document Template Manager saves as a version. |
| Target (Shared / Api / Web) | Which app a Tenant Settings category applies to. |
| Tenant | One organisation’s isolated environment. |
| Template | The form definition, including versions. |
| Azure Service Bus (ASB) | Cloud post office: FlexForms sends messages; other apps read them from a subscription. |
| Event kind | Typed (platform C# contract) or Schema (tenant JSON envelope). |
| Event type | Name on the message (TransferApplicationSubmittedEvent, LsrpPlanSubmitted, …). |
| Field mapping | JSON that copies form answers / metadata onto event properties. Does not publish until a trigger exists. |
| mappingId | Label shared by mapping JSON and the trigger. Runtime still loads by template + event type. |
| Schema event | Tenant-defined event: type name, topicName, JSON Schema. Published as SchemaEventEnvelope. |
| Subscription | Named inbox on a topic. Each consumer app needs its own. |
| Topic | Named pile of messages in Service Bus. Typed names come from CoreLibs; schema names are yours. |
| Trigger |
ApplicationSubmitted or FileUploaded plus event kind, event type, and mapping id. |
| Typed event | A platform-defined Service Bus contract. Opposite of a tenant schema event. |
| GOV.UK Notify | Government email service. FlexForms sends a template ID plus personalisation; Notify builds the email body. |
| Personalisation / placeholder | A named value Notify inserts into ((Name)) in the template. Baseline keys are always sent; extras come from EmailPlaceholderMappings. |
| EmailPlaceholderMappings | TenantConfig category that maps form field Ids (and metadata) onto Notify personalisation keys. |
| FileValidation | Optional tenant-function check of uploads (not virus scanning). Modes: Off, FailOnInvalid, RequirePassed. |
| AuthProviders | TenantConfig for machine API keys / mTLS. File-validation stores a SHA-256 KeyHash, never the raw key. |
| FormEngine | Tenant Settings (Target Web) that register search/upload complex fields: ApiEndpoint, ApiKey or client-credentials token settings, optional display expressions. |
| complexField id | The template’s complexField.id. Must match a ComplexFields Id. Does not contain URLs or secrets. |
| DropdownDisplay / ConfirmationDisplay | Optional expressions that label search results and check-your-answers text. Template values override FormEngine. |
This manual describes the Admin UI as implemented in FlexForms web. Platform SuperAdmin tools (new tenant, platform tenants) are mentioned only so tenant Admins know they are out of scope.