Skip to content

Working with Data

QuickFlo provides a set of data transformation steps for processing arrays of items — filtering rows, mapping fields, aggregating values, sorting, grouping, and joining datasets. These steps work like a data pipeline where each step transforms the output of the previous one.

Data steps that iterate over items expose these context variables, which you can use in template expressions and conditions:

VariableDescription
$itemThe current item object
$thisAlias for $item (useful for primitive arrays)
$indexZero-based iteration index
$isFirsttrue for the first item
$isLasttrue for the last item

The map step (data.map) transforms each item in an array using template expressions.

Map step field mappings with output field names and template expressions
FieldDescription
itemsArray of items to transform
mapObject defining field mappings with template expressions
includeOriginalIf true, merge transformed fields into the original item

Rename and compute fields:

Output FieldExpression
fullName{{ $item.firstName }} {{ $item.lastName }}
isAdult{{ $item.age | gte: 18 }}
email{{ $item.email | downcase }}

Extract a single value per item (flatten to primitives):

Output FieldExpression
$value{{ $item.id }}

Output: [1, 2, 3, ...] — an array of IDs instead of objects. Use $value as the field name to unwrap items into primitives.

Add a row number:

Output FieldExpression
rowNum{{ $index | plus: 1 }}
name{{ $item.name }}

Keep original fields and add computed ones:

Set includeOriginal to true and only define the new fields:

Output FieldExpression
displayName{{ $item.firstName }} {{ $item.lastName }}

All original fields are preserved and displayName is added.

{{ transform-users.items }} // transformed array
{{ transform-users.count }} // number of items
{{ transform-users.errors }} // any per-field errors

The filter step (data.filter) keeps items from an array that match a set of conditions.

FieldDescription
itemsArray of items to filter
filterConditions that items must match to be kept
Visual condition builder with ALL/ANY grouping, field expressions, and operators

Conditions are built using the visual condition builder. Each condition has three parts:

  1. Left value — typically a template expression like {{ $item.status }}
  2. Operatorequals, not equals, greater than, less than, contains, etc.
  3. Right value — the value to compare against (literal or template expression)

You can combine multiple conditions with ALL (every condition must match) or ANY (at least one must match), and nest condition groups for complex logic.

Keep active users:

Match ALL of these conditions:

  • {{ $item.status }} equals active

Keep adults with recent activity:

Match ALL of these conditions:

  • {{ $item.age }} greater than 18
  • {{ $item.lastLoginDays }} greater than 0
  • {{ $item.isDeleted }} equals false

Keep items from specific regions:

Match ANY of these conditions:

  • {{ $item.region }} equals US
  • {{ $item.region }} equals CA
  • {{ $item.region }} equals UK
{{ filter-active.items }} // items that matched
{{ filter-active.count }} // number of matched items
{{ filter-active.filteredCount }} // number of items removed

The reduce step (data.reduce) aggregates an array into summary values — like SQL aggregate functions.

FieldDescription
itemsArray of items to aggregate
reduceNamed aggregation operations
OperationDescriptionField
countCount items (optionally with a condition)
sumSum a numeric field$item.amount
avgAverage a numeric field$item.score
minMinimum value$item.price
maxMaximum value$item.price
collectGather values into an array$item.email
uniqueUnique values$item.status
countUniqueCount distinct values$item.userId
joinJoin values with separator$item.name (separator: , )
firstFirst value$item.email
lastLast value$item.status

Each aggregation is given a name (the result key) and configured with an operation and a field to operate on.

Reduce step with named aggregation operations configured in the visual editor

Multiple aggregations at once:

Result NameOperationFieldCondition
totalRevenuesum$item.amount
avgOrderValueavg$item.amount
orderCountcount
uniqueCustomerscountUnique$item.customerId
highValueCountcount$item.amount greater than 1000

Conditional aggregation:

Aggregations like count and sum can include an optional condition to only include items that match.

{{ summarize-orders.results.totalRevenue }}
{{ summarize-orders.results.avgOrderValue }}
{{ summarize-orders.results.uniqueCustomers }}
{{ summarize-orders.totalItems }}

The sort step (data.sort) orders items by one or more fields with type-aware comparison.

FieldDescription
itemsArray to sort
sortSort criteria (field, direction, type)
nullHandlingWhere to place null values: first or last

Each sort criterion has:

FieldDescription
fieldField name to sort by
directionasc or desc
typestring, number, date, or auto

Sort by creation date (newest first), then by name alphabetically:

FieldDirectionType
createdAtdescdate
nameascstring
{{ sort-users.items }} // sorted array
{{ sort-users.count }}

The group by step (data.group-by) groups items by field value(s) with optional aggregations — like SQL GROUP BY.

FieldDescription
itemsArray to group
groupByField name(s) to group by
aggregationsSame aggregation operations as the Reduce step
includeItemsIf true, include the raw items array in each group

Group sales data by region with aggregations:

Group by: region

Aggregation NameOperationField
totalSalessum$item.amount
avgDealSizeavg$item.amount
repscollect$item.repName
{{ group-by-region.groups }} // array of group objects
{{ group-by-region.totalGroups }} // number of groups
{{ group-by-region.totalItems }} // total items processed

Each group contains:

{
"key": "US",
"count": 42,
"aggregations": {
"totalSales": 125000,
"avgDealSize": 2976.19,
"reps": ["Alice", "Bob", "Carol"]
}
}

You can group by multiple fields (e.g., region and productLine), in which case each group’s key becomes an array like ["US", "Enterprise"].


The split step (data.split) divides an array into chunks or partitions.

Split into fixed-size batches — useful for processing large datasets in manageable pieces:

FieldValue
items{{ fetch-all.data }}
modechunk
chunkSize100

Output:

{{ split-batches.chunks }} // array of arrays
{{ split-batches.totalChunks }}
{{ split-batches.totalItems }}

Split into two groups based on a condition — items that match and items that don’t:

FieldValue
items{{ fetch-contacts.data }}
modepartition
condition{{ $item.status }} equals active

Output:

{{ partition-contacts.matching }} // items where condition is true
{{ partition-contacts.notMatching }} // items where condition is false
{{ partition-contacts.matchingCount }}
{{ partition-contacts.notMatchingCount }}

The select step (data.select) keeps only specified fields from each item — like SQL SELECT.

FieldValue
items{{ fetch-users.data }}
selectname, email, role

Output:

{{ select-fields.items }} // items with only the selected fields
{{ select-fields.count }}
{{ select-fields.fields }} // ["name", "email", "role"]

The join step (data.join) combines two arrays on matching key fields — like a SQL JOIN.

TypeDescription
innerOnly items that match in both arrays
leftAll from left array, matching from right
rightAll from right array, matching from left
fullAll items from both arrays

Join orders with customers on customerId = id, prefixing customer fields with customer_ to avoid name collisions:

FieldValue
left{{ fetch-orders.data }}
right{{ fetch-customers.data }}
leftKeycustomerId
rightKeyid
joinTypeleft
rightPrefixcustomer_

The dedup step (data.dedup) removes duplicate records from an array based on one or more key fields. Simpler than data.merge — no field consolidation, just deduplication.

FieldDescription
itemsArray of records to deduplicate
keyOne or more field names that define uniqueness — records with identical values across all key fields are considered duplicates
keepfirst (keep the earliest occurrence in input order) or last (keep the most recent). Defaults to first.

Use a single key (email) for simple cases or composite keys (firstName + lastName + phone) for fuzzy matching where uniqueness spans multiple fields.

{{ dedup-leads.items }} // unique records
{{ dedup-leads.count }} // number of output records
{{ dedup-leads.inputCount }} // number of input records
{{ dedup-leads.removedCount }} // number of duplicates removed

The merge step (data.merge) is dedup with field consolidation — pick a winner per group, then collect or combine values from the losing records into the winner.

FieldDescription
itemsArray of records to merge
groupByKey field(s) that define duplicate groups
winnerBy{ field, direction } — pick the winning record per group by sorting on this field (e.g., lastUpdatedAt desc)
consolidateFieldsOptional: list of { outputField, sourceFields, strategy } to roll up values from across the group. Strategies: collectUnique, first, last, concat.
distributeIntoOptional: take a collected array and spread it into named fields (e.g., phonesnumber1, number2, number3)
includeGroupCountIf true, adds _mergeCount to each output record

This is the right step when “dedup” actually means “fold N records into 1, but don’t lose data” — like merging duplicate contact records while keeping every unique phone and email across the group.

{{ merge-contacts.items }} // one record per unique group
{{ merge-contacts.count }} // number of output records
{{ merge-contacts.inputCount }} // number of input records
{{ merge-contacts.mergedGroups }} // groups that had more than one record

The scrub step (data.scrub) removes or clears records whose field values appear in a reference list. Anti-join semantics — useful for DNC scrubs, suppression lists, and any “remove the matches” pattern.

FieldDescription
itemsThe array to scrub
againstThe reference array of items to match against (e.g., DNC list)
fieldsField names on items to check
againstFieldA single field name or array of field names on the reference list to match against
actionremove (drop the entire item if any field matches) or clear (blank out only the matching field, keep the item)
{{ dnc-scrub.items }} // scrubbed array
{{ dnc-scrub.count }} // number of items in the result
{{ dnc-scrub.removedCount }} // items removed (action=remove) or fields cleared (action=clear)

The enrich step (data.enrich) adds fields from a reference dataset by matching on a key — like a VLOOKUP or a left join with field copy.

FieldDescription
itemsThe array to enrich
fromThe reference dataset to copy fields from
matchFieldField name on items to match on
fromMatchFieldField name on from items to match on
copyFieldsField names from the reference dataset to copy onto matching items
prefixOptional prefix for copied field names to avoid collisions (e.g., customer_)
{{ enrich-orders.items }} // items with new fields added
{{ enrich-orders.count }} // number of items in the result
{{ enrich-orders.matchedCount }} // how many had a matching reference record

The explode step (data.explode) takes an array of items where multiple columns hold values of the same type (e.g., number1, number2, number3 all hold phone numbers) and produces one output row per value. Inverse of pivot/spread — enables cross-record operations on values that are spread across columns.

FieldDescription
itemsArray of items to explode
columnsField names to explode into separate rows
valueFieldName of the new field on each output row that holds the exploded value (e.g., phone)
dropEmptyIf true (default), skip empty/null/undefined values
includeSourceFieldIf true (default), add a _sourceField column on each row indicating which original column the value came from

After exploding, you can data.scrub, data.filter, data.dedup, or data.group-by on the unified value field — then use data.implode to reverse the operation.

{{ explode-phones.items }} // one row per non-empty value
{{ explode-phones.count }} // number of output rows
{{ explode-phones.inputCount }} // number of input items

The implode step (data.implode) is the inverse of explode — it collapses exploded rows back into one row per group, distributing values across target columns. Use it as the last step in an explode → transform → implode pipeline.

FieldDescription
itemsThe exploded rows to recombine (typically the output of data.explode followed by data.scrub or data.filter)
groupByField(s) that identify which rows belong to the same original record. Composite keys are supported. Usually _sourceIndex from a preceding data.explode.
valueFieldName of the field on each exploded row that holds the value to pivot back into a column
targetColumnsOutput column names to distribute values into, in order. Values beyond the column count are dropped (or counted via overflowField).
preserveStrategyWhich row in each group to source non-pivoted fields from — first (default) or last
excludeFromPreserveField names to drop from the output (defaults to the bookkeeping fields added by explode: _sourceField, _sourceIndex)
overflowFieldOptional output field name that records the count of values that exceeded targetColumns capacity
sortValuesBy / sortDirectionOptional — sort exploded rows within each group before distributing (e.g., “most recent first” by lastDispoDateTime)
{{ implode-phones.items }} // one record per group
{{ implode-phones.count }} // number of output records
{{ implode-phones.inputCount }} // number of exploded input rows
{{ implode-phones.overflowCount }} // total values dropped due to target column capacity

The classic “burst, clean, recombine” pipeline. Take a contacts list with three phone columns, scrub each phone against a DNC list, then put the surviving phones back into the same three columns:

Step 1 (data.explode): items=contacts, columns=[number1,number2,number3], valueField=phone
→ produces { name, phone, _sourceIndex, _sourceField }
Step 2 (data.scrub): items=explode-step.items, against=dnc-list, fields=[phone], action=remove
→ drops rows whose phone is on the DNC list
Step 3 (data.implode): items=scrub-step.items, groupBy=[_sourceIndex],
valueField=phone, targetColumns=[number1,number2,number3]
→ reconstructs one row per original contact with surviving phones

After this pipeline, contacts that had all phones scrubbed disappear entirely; contacts that had some phones scrubbed keep their non-phone fields and have the surviving phones packed into the leftmost target columns.


These additional data.* steps cover format conversions, key remapping, classification, diffs, and base64 encoding. Each is configured through the same form-based editor — pick the operation, point it at your data, and reference the output by step ID.

StepWhat it does
data.flattenFlatten a nested array field one or more levels deep
data.map-keysRename object keys via a mapping (with optional drop of unmapped keys)
data.classifyTag items into named groups based on JSONLogic rules; supports multi-group tagging via tagField
data.diffCompare two arrays by key field(s) and report added, removed, and changed records
data.csv-to-itemsParse a CSV (literal, file URL, or upload) into an items array
data.transform-csvCSV-in / CSV-out transform when you don’t need a typed items array in between
data.items-to-csvRender an items array back into a CSV string
data.encode-base64Base64-encode a string
data.decode-base64Base64-decode a string

Data steps are most powerful when chained together. Each step’s output feeds into the next:

fetch-data → filter-active → map-fields → sort-by-date → split-batches → for-each → process

Example pipeline:

  1. Filter to keep only active records
  2. Map to transform field names and compute values
  3. Sort by date descending
  4. Split into batches of 100
  5. For-each over batches to process in parallel
// Step 1: filter
{{ filter-active.items }} // filtered array
// Step 2: map (takes filter output)
{{ map-fields.items }} // transformed array
// Step 3: sort
{{ sort-results.items }} // sorted array
// Step 4: split into batches
{{ split-batches.chunks }} // array of 100-item arrays
{{ split-batches.totalChunks }} // number of batches