Skip to main content
TL;DR: Use the Repeater component for n-item line entries (equipment checks, audit findings, inventory counts). Use renderWhen for conditional follow-up questions when an item fails inspection. Store schemas in a database so inspection templates can be updated by operations staff without a deployment. Integrate file upload for photo evidence. Last reviewed: April 2026.
FormEngine Designer — manage inspection templates visually without code

The problem

Field inspections, audits, and structured data-collection workflows share a set of requirements that generic form libraries handle poorly:
  • Variable line items. An equipment inspection might cover 3 machines one visit and 15 the next. The form needs to handle a dynamic number of identical entry groups.
  • Conditional follow-ups. If a piece of equipment fails a check, a failure description field becomes required. If it passes, that field is irrelevant and shouldn’t appear.
  • Template updates without dev involvement. The safety team adds a new check after a regulatory change. Operations can’t wait for a sprint.
  • Evidence capture. Photo attachments, signatures, and GPS metadata belong next to the inspection item they document — not at the end of a flat form.

Repeater: n-item inspection entries

The Repeater component manages a dynamic array of identical field groups. Users add and remove items at runtime; each item maintains its own field values.
{
  "type": "Repeater",
  "name": "equipmentChecks",
  "props": {
    "label": "Equipment Inspection Items",
    "addLabel": "Add Equipment",
    "removeLabel": "Remove"
  },
  "components": [
    {
      "type": "Input",
      "name": "equipmentId",
      "props": { "label": "Equipment ID / Tag", "required": true }
    },
    {
      "type": "Dropdown",
      "name": "checkResult",
      "props": {
        "label": "Inspection Result",
        "required": true,
        "options": [
          { "label": "Pass", "value": "pass" },
          { "label": "Fail", "value": "fail" },
          { "label": "Not Applicable", "value": "na" }
        ]
      }
    },
    {
      "type": "Textarea",
      "name": "failureDescription",
      "props": { "label": "Describe the failure", "required": true },
      "renderWhen": "form.data.checkResult === 'fail'"
    },
    {
      "type": "Uploader",
      "name": "evidencePhotos",
      "props": { "label": "Photo Evidence", "multiple": true },
      "renderWhen": "form.data.checkResult === 'fail'"
    },
    {
      "type": "Dropdown",
      "name": "severity",
      "props": {
        "label": "Failure Severity",
        "required": true,
        "options": [
          { "label": "Critical — stop work", "value": "critical" },
          { "label": "Major — repair within 24 hours", "value": "major" },
          { "label": "Minor — repair at next service", "value": "minor" }
        ]
      },
      "renderWhen": "form.data.checkResult === 'fail'"
    }
  ]
}
Inside a Repeater item, form.data refers to that item’s own fields. form.data.checkResult reads the current item’s result, not the parent form’s data. So renderWhen: "form.data.checkResult === 'fail'" correctly hides/shows follow-up fields per item — each item’s conditional logic is independent.

Accessing parent context inside Repeater items

Sometimes an item’s logic depends on data outside the Repeater — for example, the inspection type selected at the top of the form might determine which severity scale to use. Access parent-level data via form.parentData:
{
  "type": "Dropdown",
  "name": "severity",
  "renderWhen": "form.data.checkResult === 'fail' && form.parentData.inspectionType !== 'visual-only'"
}
form.parentData is the outer form’s data object. form.rootData is always the root level regardless of Repeater nesting depth.

Section-level conditional logic

Entire sections of an inspection form can appear or disappear based on the inspection type. Use a Container with renderWhen:
{
  "type": "Container",
  "name": "electricalSection",
  "props": { "label": "Electrical Systems" },
  "renderWhen": "form.data.inspectionType === 'electrical' || form.data.inspectionType === 'full'",
  "components": [
    {
      "type": "Repeater",
      "name": "electricalChecks",
      "props": { "label": "Electrical Check Items", "addLabel": "Add Check" },
      "components": [...]
    }
  ]
}
A “Visual Only” inspection type hides the entire Electrical Systems section (and skips all its validations). A “Full” inspection shows it.

Signature capture for sign-off

Inspections typically require a sign-off from the inspector and sometimes a secondary approver. The Signature component captures a drawn signature:
{
  "type": "Signature",
  "name": "inspectorSignature",
  "props": {
    "label": "Inspector Signature",
    "required": true
  }
},
{
  "type": "Input",
  "name": "inspectorName",
  "props": { "label": "Inspector Name", "required": true }
},
{
  "type": "Input",
  "name": "inspectorLicense",
  "props": { "label": "License Number" },
  "renderWhen": "form.data.inspectionType === 'electrical' || form.data.inspectionType === 'full'"
}
The signature value is stored as a base64 data URL in form.data.inspectorSignature. Store it in your database or attach it to the generated PDF report.

Computed summary field

Show the inspector a running count of failures before submission using a computed Label:
{
  "type": "Label",
  "name": "failureSummary",
  "props": {
    "text": {
      "computeType": "function",
      "value": "const items = form.data.equipmentChecks || []; const fails = items.filter(i => i.checkResult === 'fail').length; return fails === 0 ? 'No failures found.' : `${fails} failure${fails > 1 ? 's' : ''} recorded — review before submitting.`"
    }
  }
}
The label updates live as the inspector works through items. No server round-trip, no reload.

Complete form structure

A realistic equipment inspection form puts it all together:
{
  "type": "Container",
  "name": "inspectionRoot",
  "components": [
    {
      "type": "Dropdown",
      "name": "inspectionType",
      "props": {
        "label": "Inspection Type",
        "required": true,
        "options": [
          { "label": "Visual Only", "value": "visual-only" },
          { "label": "Electrical", "value": "electrical" },
          { "label": "Full", "value": "full" }
        ]
      }
    },
    {
      "type": "Input",
      "name": "siteId",
      "props": { "label": "Site / Location", "required": true }
    },
    {
      "type": "Datepicker",
      "name": "inspectionDate",
      "props": { "label": "Inspection Date", "required": true }
    },
    {
      "type": "Repeater",
      "name": "equipmentChecks",
      "props": { "label": "Equipment", "addLabel": "Add Equipment" },
      "components": [...]
    },
    {
      "type": "Container",
      "name": "electricalSection",
      "renderWhen": "form.data.inspectionType === 'electrical' || form.data.inspectionType === 'full'",
      "components": [...]
    },
    {
      "type": "Label",
      "name": "failureSummary",
      "props": { "text": { "computeType": "function", "value": "..." } }
    },
    {
      "type": "Signature",
      "name": "inspectorSignature",
      "props": { "label": "Inspector Signature", "required": true }
    },
    {
      "type": "Button",
      "name": "submitBtn",
      "props": { "label": "Submit Inspection Report" },
      "actions": [
        { "event": "onClick", "action": "validate" },
        { "event": "onClick", "action": "submit" }
      ]
    }
  ]
}

Submitting the data

<FormViewer
  view={view}
  form={JSON.stringify(schema)}
  initialData={{ inspectionDate: new Date().toISOString().split('T')[0] }}
  onActionEventAsync={async (event) => {
    if (event.name === 'submit') {
      const report = event.args.formData
      // report.equipmentChecks is an array of item objects
      // report.inspectorSignature is a base64 data URL
      const response = await fetch('/api/inspections', {
        method: 'POST',
        body: JSON.stringify(report),
        headers: { 'Content-Type': 'application/json' }
      })
      const { reportId } = await response.json()
      navigate(`/inspections/${reportId}/report`)
    }
  }}
/>
initialData pre-sets today’s date so the inspector doesn’t have to pick it. report.equipmentChecks is a flat array of objects — one object per Repeater item — ready to insert into a database or attach to a PDF generator.

Template management by operations staff

With FormEngine Designer embedded in an admin panel, your safety or operations team can:
  • Add a new check item to an inspection type
  • Change pass/fail options to pass/fail/partial
  • Add a new section for a new equipment category
  • Require a photo only when severity is “Critical”
All without involving engineering. The updated schema takes effect the next time a technician opens the inspection form.
// Admin panel: operations manager edits an inspection template
<FormBuilder
  view={view}
  initialForm={currentInspectionSchema}
  onSave={async (updatedSchema) => {
    await db.inspectionTemplates.update({
      type: 'equipment',
      schema: updatedSchema,
      updatedAt: new Date()
    })
  }}
/>


FormEngine on GitHub · npm · Pricing
Last modified on April 16, 2026