A question in the Quickbase community, Workflow, asked something every pipeline builder runs into sooner or later: how do you build workflows that don't create duplicates when they run more than once?
The goal has a name: idempotent. A pipeline is idempotent when running it twice with the same input leaves your data exactly as running it once would. Reruns, double clicks, edits that re-fire a trigger: none of them should add a second record.
The recipe is short. Give every record you create a key that says what it is, make that key Unique, and write with upsert instead of create. Then close the trigger gaps that cause reruns in the first place.
The example
Say you have an Inspections table and a Checklist Items table. When an inspection is scheduled, a pipeline creates one Inspection Task for each checklist item and assigns it to someone like Sarah Lee or Mike Chen.
Without a key, every time that pipeline fires for inspection 1042, you get another full set of tasks. With a key, the second run finds the tasks it already made and updates them.
Step 1: Design the key
The key answers one question: what makes this record the one I mean? For an inspection task, that's "this inspection plus this checklist item." So:
Task Key = [Inspection Record ID#] & "-" & [Checklist Item Record ID#]
Inspection 1042 and checklist item 7 always give 1042-7, no matter how many times the pipeline runs.
A few rules that keep keys reliable:
- Build it from Record IDs, not names or dates. Names get edited and two people can share one. Record ID# never changes.
- Include every part that makes the record distinct. If a task can legitimately exist once per inspection per week, the week belongs in the key too.
- Use a separator. Without the dash, inspection 1 + item 23 and inspection 12 + item 3 both become
123. - Build it the same way everywhere. If one step lowercases or trims a value and another doesn't, the keys won't match. That matters most when part of the key is typed text, like an email.
Step 2: Add a Unique key field to the target table
On the table the pipeline writes to (Inspection Tasks):
- Go to the table's Fields and add a Text field called Task Key.
- Open its properties and check Unique. I also check Required so a record can't slip in with no key.
- Save.
This is your safety net. Even if a pipeline is misconfigured, Quickbase will refuse a second record with the same Task Key. If Quickbase won't let you turn on Unique, the table already has duplicate values; clean those up first (a summary report grouped by the field shows them quickly).
The pipeline fills this field itself, so it doesn't need to appear on forms.
Step 3: Write with upsert instead of Create Record
In Pipelines, replace the Create Record step with three steps from the Quickbase channel under Bulk Record Sets:
- Prepare Bulk Record Upsert: table = Inspection Tasks, pick the fields you'll write, and set the merge field to Task Key.
- Add a Bulk Upsert Row (inside your loop over checklist items, if you have one): map Task Key to
{{a.id}}-{{b.id}}(your inspection step and checklist item step), plus Task Name, Assigned To and so on. - Commit Upsert, after the loop. Nothing is written until this step runs.
When the key already exists, that row is updated. When it doesn't, a new record is created. Quickbase's help article on bulk record sets covers each step.
If you need to create a parent record or link to an existing one, I walked through that version in Create or Link a Parent Record in Quickbase Pipelines. Same idea, with a search-and-link step after the upsert.
One thing to decide: an upsert overwrites every field you map. If people edit tasks by hand after they're created (say, reassigning from Sarah Lee to Mike Chen), map only the fields the pipeline owns, so a rerun doesn't undo their changes.
Step 4: Stop the trigger from firing when it shouldn't
The key catches duplicates. These fixes stop the extra runs that cause them.
"On New Event" fires on edits too
On New Event can fire when a record is added and when it's modified. If your pipeline is meant to run once when an inspection is scheduled, every later edit (a new note, a date change) runs it again.
- If it should run only on new records, use Record Created, or set On New Event to fire on adds only.
- If it really needs to run on updates, limit it to specific fields changing (for example, only when Status changes), and add a condition for the value you care about (Status is Scheduled).
Checkbox triggers loop or repeat
A "Create Tasks" checkbox that a user ticks to kick off a pipeline is handy, but it fires again every time the box is ticked. Guard it on both ends:
- Add a Tasks Created On date/time field to the Inspection.
- In the trigger condition, require Create Tasks is checked and Tasks Created On is empty.
- As the last step, update the Inspection: uncheck Create Tasks and set Tasks Created On to now.
That last update is itself an edit, and unchecking the box changes the very field you trigger on. The condition is what keeps that from turning into a loop: on the re-fire, the box is unchecked and the date is filled, so the pipeline skips.
Linking by name or date
If a step finds "the right" parent by searching on a name or date, two records with the same name, or a date that shifted, will link the wrong record or none at all, and the next step may create a new one. Pass and match Record ID# wherever you can.
What happens when the Unique field rejects a write
I tested this in a sandbox table with a Unique, Required Task Key field, using the JSON API so I could see the exact responses.
| What I sent | Result |
|---|---|
Upsert, key 1042-7, assigned to Sarah Lee | Created Record ID# 1 |
| Same upsert again, assigned to Mike Chen | Updated Record ID# 1 |
| Same upsert again, same values | Unchanged Record ID# 1 |
Plain insert (no merge field), key 1042-7 | Rejected: Adding non-unique value for field with ID "8". |
| Plain insert of two rows, one new key and one duplicate | New one created, duplicate rejected |
| Upsert with a non-unique field as the merge field | Refused: Merge field must be unique |
The table ended with one task for 1042-7, which is the point.
Two details worth knowing:
- A rejected row doesn't fail the whole request. The plain insert came back with HTTP 207 and the error listed under
lineErrorsfor that row, while the other row in the same batch was saved. If you're checking only for "did the call succeed?", readlineErrorstoo. - The rejection is a signal, not just a block. If a Create Record step starts hitting the Unique field, something is running twice. Switch that step to upsert and check the trigger.
Using upsert from the JSON API
Outside Pipelines, the same pattern is one call. The Insert/Update records endpoint takes a mergeFieldId: the field ID of your Unique key.
POST https://api.quickbase.com/v1/records
{
"to": "<table id>",
"mergeFieldId": 8,
"data": [
{
"6": { "value": "Check fire exits" },
"7": { "value": "Sarah Lee" },
"8": { "value": "1042-7" }
}
],
"fieldsToReturn": [3]
}
The response tells you what happened to each row:
"metadata": {
"createdRecordIds": [],
"updatedRecordIds": [1],
"unchangedRecordIds": [],
"totalNumberOfRecordsProcessed": 1
}
Leave out mergeFieldId and the same call becomes a plain insert, which is exactly the one the Unique field rejected above. Keep it in any script that might be retried.
Common snags
- Records still duplicate after switching to upsert. Compare the keys on the duplicates. Usually one path builds the key slightly differently (extra space, different case, a name instead of an ID).
- Nothing gets written. Check that Commit Upsert exists and runs after the loop. Quickbase also fixed an issue in October 2026 where Commit Upsert sometimes didn't run after a loop and the pipeline sat in a running state for 30 minutes (release note).
- People get an error adding a record by hand. That's the Unique field doing its job. Tell your users what the key means, or keep manual entry off that table.
- Edits by users get overwritten. You're mapping a field the pipeline doesn't own. Remove it from the upsert.
Quick checklist
- A key built from Record IDs, with a separator
- A Unique (and Required) text field holding that key
- Upsert with that field as the merge field, in Pipelines or the API
- Triggers that fire only when they should, with a "processed" stamp for checkbox triggers
- Links by Record ID#, never by name or date