0. Intro
In this tip I’ll show you how to dynamically add new rows to a form (Repeating Group), fill inputs in them and bulk create multiple things with a single button click avoiding:
- any plug-in or JS (while using JS will be much easier - I try to avoid it until I have no choice
) - creating empty things and using auto-binding
As an example we will be creating new Tasks. Data type Task has 2 fields - task name and task description (both are “text” fields).
Here is what we will get in the end:

1. Creating custom states
We will need two custom states (CS further in the text):
row_numbers[state type = number, list = yes]. This CS will be used to collect numbers (integers) and add new rows in the Repeating Group. Also it will serve as a reference during bulk things creation.rows_content[state type = text, list = yes]. This CS will be used to accumulate values from each individual row.
My custom states are defined at a page level (page name = rg_row_addition), so you’ll see expressions like rg_row_addition's row_numbers and rg_row_addition's rows_content below.
2. Creating a form with dynamic rows addition/deletion
2.1 Setting up RG
Add a Repeating Group (RG further in the text):
- type of content = number
- data source = CS
row_numbers
Add required input elements to our RG. In my example I need 2 inputs to enter task name and task description:
2.2 Setting up the logic to add and delete rows
2.2.1 Adding rows
Add a button/icon that will be used to add new rows to the RG. In the WF we will use set state action with the following expression:
Expression breakdown
row_numbers CS is a list of numbers (empty on page load). When button/icon is clicked we are using :plus item to add a new number to the list. The number to be added is determined as “find the highest number in the list and +1 to it”. So when our row_numbers is empty - we will add number 1 to the list and the list will look like [1]. When row_numbers value is [0,1,2,3,4] - we will add number 5, and so on.
2.2.2 Deleting rows
To delete a row add an icon in the RG cell. When clicked → set state action and remove current cell’s number from row_numbers CS:
2.2.3 Testing results
After completing steps 2.2.1 and 2.2.2 our logic is ready:

Pay attention to the fact that row number isn’t always equal to RG’s cell index. If we have 3 rows and delete the Row #2 → Row #3 will have cell index #2. This will add us some issues like reseting inputs values while deleteing any row except the last one. Check 3.4 Fixing issues with updating indexes after row deletion for more info.
3. Storing content from each row
Before diving into details I’ll describe the general idea of the approach. We will use CS rows_content to accumulate content from each individual row of the RG:
- Row content storing format: each item (text) of
rows_contentstores values according to the following template:number/input’s A value/input’s B value - Row content update: we are going to update CS
rows_contentafter any input’s element value change.
⚠️ important note about text separator choosing
for simplicity in my example scenario I chose / separator, but keep in mind that if users will use / in input field - everything will break. So it’s better to use something that users will not type, like ///// or whatever else.
3.1 Creating a custom event to add/modify row content to CS
As I mentioned above - we need to trigger WF each time any input element’s value is changed. We will follow DRY principle and use Custom Event.
Create a custom event (”collect values” in my example):
Parameters:
- row_number (type = number)
- task name (type = text)
- task description (type = text)
In the custom event add action set state:
Expression breakdown
rg_row_addition's rows_content:filtered:count > 0→ in this part we are checking if there is already an entry (text) in CSrows_contentfor our row number.:filtered: we are splitting our item (text) using/separator.:first itemwill be the row number (as a text), that’s why we need to convert it to number, then compare with row number:
:formatted as text: the result of previous part is a boolean (yes/no). This is where the power of:formatted as textoperator arise:
- formatting for no: if there is no item for row number → add it to the list.
Arbitrary text:
- formatting for yes: if the item for row number already exists in the list → we need to update it. This can be achieved by deleting old value from the list (
:minus item) and adding new version (:plus item).Arbitrary textis the same as in formatting for no.
- formatting for no: if there is no item for row number → add it to the list.
:split by(,): applying:formatted as textevaluates to a text. But our CSrows_contentis a list of texts. So we need to convert the result of previous expression part into a list. Unfortunately, Bubble doesn’t provide us with:coverted to listoperator for text type. As a workaround we can use:split byoperator. Text separator should be,
3.2 Setting up WFs to trigger on inputs values changes
Now we need to create a workflow for each input element in our RG.
- Trigger: when input’s value is changed
- Action:
trigger custom event“collect values”
I have 2 input fields (task name and task description) - so I have 2 workflows. The only difference between them is the trigger (input “task name” or input ‘task description” values changed) while the action (trigger custom event) is the same.
3.3 Testing results
Check filling and editing your inputs:

3.4 Fixing issues with updating indexes after row deletion
If you have multiple rows with filled input elements and delete any row (except the last one) - all inputs will be reset:

I suppose the problem root cause is tied to recalculating cells indexes. The good news is that we still have inputs values in our rows_content CS. So all we need is just to use conditional initial content for each input element that will reference values from the CS.
will update this section tomorrow![]()
4. Bulk creating Things
To bulk create things (tasks in my example) we will use a recursive API WF.
Params
row_numbers and rows_content store values from our custom states. Param index will be used to iterate through row_numbers and to stop recursive WF rescheduling after last number from row_numbers is processed.
In Create a new Thing action I’m defining tasks name and description:
Expression breakdown
- From
rows_contentlist we extract the value that matches the number defined inindex:
- Split the text we extracted in the previous step by
/and get a list of 3 values (number, task name, task value). For task name we will use:item #2and for task name:item #3.
The last thing to do is to reschedule our API WF:
5. Testing the final result

6. Bonus: bulk updating multiple Things values in RG with a single button click
You can use the idea from this tip to bulk update existing Things (just skip step #2 and start with step #3 Storing content from each row. For example, you can solve this task (you’ll need to adapt #3.1 Creating a custom event to add/modify row content to CS in the part where :split by operator is used to catch issues with , in prices):




















