Block Types Reference covers the shape a grid's data needs to end up in - a flat array of cells, two per row for a 2-column grid, three for a 3-column grid, and so on. Typing that array out by hand works for a short, fixed table. This page is about building that array from real submitted data - most commonly a repeating group (see Form Editor), where the number of rows isn't known ahead of time.

The simplest option: bind directly, no JS at all

If a grid just needs one column per sub-field of a repeating group - no custom formatting, no computed values - skip data entirely and set source_field / column_keys on the grid block instead:

{
  "id": "options_grid", "type": "grid", "columns": 2,
  "source_field": "options",
  "column_keys": ["description", "price"]
}

This flattens inputs.options (one object per submitted row) into cells automatically

The moment you need formatting (currency signs, right-aligned prices, a header row, computed text) - reach for data instead; a grid with data set ignores source_field/column_keys entirely. The rest of this page covers that path.

The straightforward way: loop and push

Say a repeating group field called options has two sub-fields, description and price. Each submitted row shows up in inputs.options as an object with those two keys. To turn that into grid cells - a numbered description on the left, a right-aligned price on the right - loop over it and push two cells per row:

inputs.gridData = [];
for (var i = 0; i < inputs.options.length; i++) {
  var option = inputs.options[i];
  inputs.gridData.push(
    { text: (i + 1) + '. ' + option.description },
    { text: '$' + option.price, 'text-align': 'right' }
  );
}

Read top to bottom: start with an empty array, walk each submitted option, and for each one add its two cells in order. Array.push happily takes more than one value at a time, so both of a row's cells go in with one call.

Put this in process.js (see below), then reference it from the grid block:

{ "id": "options_grid", "type": "grid", "columns": 2, "data": "inputs.gridData" }

Adding a header row

Push the header cells once, before the loop, using the same object shape:

inputs.gridData = [
  { text: 'Description', 'font-weight': 'bold' },
  { text: 'Price', 'font-weight': 'bold', 'text-align': 'right' },
];
for (var i = 0; i < inputs.options.length; i++) {
  var option = inputs.options[i];
  inputs.gridData.push(
    { text: option.description },
    { text: '$' + option.price, 'text-align': 'right' }
  );
}

A more compact alternative: .flatMap()

Once the loop-and-push version feels familiar, the same logic can be written as one expression with .flatMap() - each option maps to its two cells, and flatMap flattens all those pairs into one array automatically:

inputs.gridData = inputs.options.flatMap(function (option, i) {
  return [
    { text: (i + 1) + '. ' + option.description },
    { text: '$' + option.price, 'text-align': 'right' },
  ];
});

This produces exactly the same array as the loop above - it's a style preference, not a different feature. Reach for it once you're comfortable with it; the loop version is just as correct and often easier to read back later, especially once a header row or some conditional formatting gets added in.

Where this code lives

Both versions above can go straight in the grid block's own data field instead of process.js, wrapped as a single expression (an immediately-invoked function, since a for loop needs statements, not just an expression):

"data": "(function(){ var cells = []; for (var i = 0; i < inputs.options.length; i++) { var o = inputs.options[i]; cells.push({text: (i+1)+'. '+o.description}, {text: '$'+o.price, 'text-align':'right'}); } return cells; })()"

...but that's awkward to read and edit as a single JSON string. Once the logic is more than a one-liner, it's worth moving to process.js instead - assign it to a new property on inputs (inputs.gridData above), then reference that property from the block. See process.js -> Handing computed data to blocks for exactly how that mechanism works and the couple of things to know about it (new property names only, separate scripting context from the block itself).