FormWork FormWork
engineering 6 min read

How We Model Complex Repeatable Data in Forms

How FormWork models repeatable data so nested repeatable fields, groups, calculations, and workflows remain predictable as forms grow.

Andrius Bartulis ·

A common requirement in complex form-centric systems is the need to capture repeated structured data: invoice line items, household members, products in an order, steps in a process. Each item has its own fields, users can add and remove entries, there are often multiple nested levels of these repeated data items, and the rest of the system still needs to make sense of the result and work with it reliably.

FormWork was built from the ground-up to handle this well.

Why repeatable data is tricky to work with

At first, repeatable groups sound simple:

“Let the user add as many items as they need.”

But there are many details that makes this more involved than it seems:

  • each item can have nested fields
  • users can add, remove, and reorder items
  • calculations may depend on both item-level and form-level values
  • workflows may need to update a specific item in a dynamic tree structure of answers
  • exports and API consumers still need consistent structure

Many tools represent repeatable groups as single depth arrays tied closely to the current UI order. That can work for basic cases, but it becomes fragile once users start editing data in the middle of the list, or need multi-level data.

How we model repeatable data in FormWork

In FormWork, repeatable groups are stored as structured data with stable instance identity that works even with multiple levels of repeatable data nesting.

Each item gets its own instance key, and the repeater field itself stores the current instance order.

That means there are two related pieces of data:

  • the list of instance names for that repeater, in display order
  • the answer paths stored under each instance name

In simplified form, it looks like this:

{
  "line_items": ["inst2", "inst1"],
  "line_items[inst1].product_id": "sku_123",
  "line_items[inst1].qty": 2,
  "line_items[inst1].unit_price": 25,
  "line_items[inst2].product_id": "sku_456",
  "line_items[inst2].qty": 1,
  "line_items[inst2].unit_price": 40
}

Here, line_items stores the current order of instances, while inst1 and inst2 identify the actual records.

That means a user can reorder items without changing what those items are. A workflow can update the right instance. A calculation can refer to the same item after edits. The structure stays stable even when the interface changes.

Nested repeatable field groups

The same architecture works with nested repeatable data.

For example, an order line item might have:

  • the product and quantity
  • a repeatable list of option selections
  • a repeatable list of per-line adjustments or discounts

Or a household application might have:

  • a repeatable list of people
  • and, for each person, a repeatable list of addresses, documents, or income sources

Most systems simply do not support this well. They may handle one repeater, but once a repeated item contains another repeater, teams usually end up writing custom code around the form.

FormWork was designed for this from the beginning, so the same model applies at every level.

The same rule still applies: each instance needs its own identity at every level.

Each nested repeater also stores its own ordered list of instances under its parent instance. Here is an example of how such data is represented:

{
  "line_items": ["inst2", "inst1"],
  "line_items[inst1].product_id": "sku_123",
  "line_items[inst1].qty": 2,
  "line_items[inst1].options": ["opt2", "opt1"],
  "line_items[inst1].options[opt1].label": "Gift wrap",
  "line_items[inst1].options[opt1].price": 5,
  "line_items[inst1].options[opt2].label": "Rush processing",
  "line_items[inst1].options[opt2].price": 12,
  "line_items[inst2].product_id": "sku_456",
  "line_items[inst2].qty": 1,
  "line_items[inst2].options": ["opt1"],
  "line_items[inst2].options[opt1].label": "Extended warranty",
  "line_items[inst2].options[opt1].price": 20
}

The important detail is that ordering and identity are separate at each level:

  • line_items stores the order of line item instances
  • line_items[inst1].options stores the order of option instances for inst1
  • opt1 under inst1 is not the same thing as opt1 under inst2

That makes the full answer path unambiguous, and any particular tree branch can be referenced by combining all the answer instance paths from the top most parent to the deepest level we are interested in.

This matters for a few practical reasons.

  • removing one option from a line item should not change the identity of the remaining options
  • reordering nested items should not break references from calculations or workflows
  • validation needs to know exactly which nested instance produced an error
  • workflows need to be able to iterate at the right level and write back to the right parent or child item

Without that structure, teams often flatten nested data into text blobs, create custom JSON fields, or move logic out into application code. That can work, but it usually makes the form harder to validate, harder to evolve, and harder to integrate with the rest of the system.

Our approach is to treat nested repeatables as the same structured data problem, just one level deeper. The model stays explicit enough that you can read it, reference it, validate it, and update it without guessing which item is which.

Why that matters beyond storage

This choice affects the rest of the platform.

Conditional logic

Conditions can be evaluated against repeatable and nested values without treating them as a special case. You can define rules inside any depth of a multi-level nested repeatable field group, for instance with the previous example a display logic rule could be added to only show the price field for the option once the option has been selected. The conditional logic system can work with references to siblings, or any ancestor level of the nested repeatable group data structure.

Validation

Similarly, validation runs against the current visible structure and the actual instance paths, including nested repeatable data.

Workflows

Workflows can iterate through repeatable collections with FOR_EACH, access the current item, and write values back to that same instance.

Data References

References work across repeatables in the same way they work elsewhere, including inside iteration contexts.

Calculations

You can calculate values per item and then aggregate them into totals, scores, or summaries without losing track of where each value came from.

A concrete example

Consider an order form with repeatable line items:

  • product
  • quantity
  • unit price
  • optional discount
  • line total

In practice, the form often also needs product lookups, per-line calculations, and an overall order total.

The basic flow is:

  1. Iterate through each line item.
  2. Read the values for that item.
  3. Calculate the line total.
  4. Write the result back to that same item.
  5. Aggregate all line totals into an order total.

This is not unusual. It is a common pattern in operational forms. The main requirement is that each repeated item remains identifiable throughout the process.

What we think matters

For repeatable data to stay manageable over time, a platform should provide a few basic guarantees:

  1. Each repeated item should have stable identity.
  2. Repeatable field groups should be nestable with no limits.
  3. The repeatable field concept must be designed as a core primitive, with display logic, validation and other systems being able to fully support it.
  4. Workflows should be able to work with multi-level repeatble field groups natively.
  5. Schema changes should not make old repeatable field submissions ambiguous.

These decisions mean that FormWork can support complex data form scenarios that are simply not possible in most no-code form builders.

Tags repeatable fieldsnested formsworkflow automationcomplex formsform architectureagenciesformwork