The three data models, mapping and transforming fields, lists and loops and aggregation, storing data inside the platform, and files.
A Signal Through field guide. Independent and vendor-neutral; not affiliated with n8n GmbH, Celonis (Make), or Zapier Inc. Approximately 23725 words.
In this volume:
Every workflow automation is, underneath everything else, a machine for moving structured information from one place to another. A form submission becomes a spreadsheet row. An invoice becomes a Slack message. A new customer becomes three records in three systems. The apps at either end speak the same underlying language — a text format called JSON — but the three platforms in this guide present that language to you in strikingly different costumes. Zapier dresses it up as a flat list of friendly named fields. Make wraps it in packages called bundles and shows you the wrapping. n8n hands you the raw JSON and a good pair of gloves.
Chapter 3 (Mental Models: How a Run Actually Happens) previewed this. This chapter is the deep version, and it is deliberately theory-and-inspection only: you will learn what each platform's data actually looks like, where to find a step's real input and output, and how to answer the single most useful debugging question in automation — "what did this step really receive?" Changing values comes in Chapter 12 (Mapping and Transforming Fields), and handling many records at once comes in Chapter 13 (Lists, Loops, and Aggregation). You cannot do either well until the shapes in this chapter feel familiar.
One promise before we start: the data model is not a detail. It is the main reason the three platforms feel so different to use. By the end of this chapter you will understand why Make handles "a list of things" gracefully, why n8n feels like programming, and why Zapier's simplicity is a deliberate flattening — with a cost that appears exactly when your data stops being simple.
JSON (pronounced "JAY-son", short for JavaScript Object Notation) is the bracketed text format that web apps use to exchange data. When your CRM (customer relationship management tool — the app where your contacts live) sends a contact to your email tool, what actually travels over the wire is a block of JSON text — developers call that block a payload, the data an app packs into a single message. All three platforms receive JSON, hold JSON internally, and send JSON — they just differ in how much of it they show you. Learn to read it once and you can debug anything on any of the three.
Here is a small, realistic JSON document — one contact record — followed by a guided tour:
{
"name": "Priya Raman",
"email": "priya@example.com",
"vip": true,
"orders_this_year": 4,
"referral_source": null,
"company": {
"name": "Bluewater Outfitters",
"size": "11-50"
},
"tags": ["newsletter", "webinar-signup"]
}Reading it line by line:
{ } mark an
object — a container of labelled values. Think of it as
a single record, or a filing-card with named boxes on it."name": "Priya Raman" means "the field called name holds
the text Priya Raman." Keys are always quoted; a comma separates each
pair from the next."Priya Raman" is a string
(text — always in double quotes). 4 is a
number (no quotes). true is a
boolean (a pure yes/no value; the only options are
true and false, unquoted). null
is the deliberate "nothing here" value — the record has a
referral_source box, and the box is explicitly empty.company holds another pair of curly braces. That is an
object inside an object — a nested record. Priya's
company has its own name and its own size, grouped together rather than
sprayed into the top level. Nesting is how JSON keeps related things
related.tags holds square brackets [ ] — an
array, JSON's word for a list. This one is a list of
two strings. Arrays are ordered, can be any length including zero, and
can contain anything: strings, numbers, or entire objects.That last possibility — an array of objects — is the shape that matters most in automation, because it is how the real world says "several of something." An order with three line items, an email with two attachments, a search that returned five contacts: all arrive as an array of objects. Here is the shape in miniature:
"items": [
{ "sku": "MUG-11", "quantity": 2 },
{ "sku": "TEE-M", "quantity": 1 }
]Square brackets open a list; each element of the list is itself a
curly-braced object with its own fields. (A sku, if the
term is new, is a stock-keeping unit — a product's short inventory
code.) How each platform handles exactly this shape is the heart of this
chapter.
A summary table for reference:
| JSON syntax | Name | What it means | Example |
|---|---|---|---|
"text in quotes" |
string | Text, including numbers-as-text like postal codes | "Priya Raman" |
42 or 3.5 |
number | A quantity you could do math on | 4 |
true / false |
boolean | A pure yes/no | true |
null |
null | Explicitly nothing | null |
{ ... } |
object | A record of named fields; can nest | {"name": "..."} |
[ ... ] |
array | An ordered list of values or objects | ["a", "b"] |
Tip: Raw JSON often arrives as one long unbroken line. Paste it into any "JSON formatter" you find with a quick web search (or a code editor such as VS Code, which can format JSON built-in) and it will be indented into the readable layered shape shown above. Thirty seconds of formatting saves ten minutes of squinting.
Watch out: "Missing," "null," and "empty" are three different things in JSON, and automations break on the difference. A record can lack the
phonekey entirely, have"phone": null, or have"phone": ""(an empty string). A filter that checks "phone exists" may pass the empty string; a step that requires text may choke on null. When a workflow misbehaves around a blank-looking field, inspect the raw data and establish which of the three you actually have.
With the vocabulary in place, we can look at what each platform does with it.
Zapier's founding bet was that most business automation moves one record at a time, and that the person building the automation should never have to see brackets. So when a step in a Zap (Zapier's word for a workflow) produces data, Zapier flattens it: every value, however deeply nested in the original JSON, becomes a single named field in one long flat list.
Take Priya's record from above. In Zapier's editor you would not see
the company object as a nested structure. You would see
fields with names like Name, Email,
Vip, Company Name, and
Company Size — the nesting collapsed into compound labels,
each with a sample value shown beside it. When you set up a later step
and click into one of its input boxes, Zapier opens a dropdown — this
book will call it the field picker — listing every
field from every earlier step, searchable, with those sample values
displayed so you can pick "the thing that looks like an email address"
without knowing or caring where it lived in the JSON tree.
This is genuinely the friendliest presentation of the three, and for flat, one-at-a-time records — a form fill, a new CRM contact, a calendar event — it is close to ideal. The cost appears when the data contains an array.
Zapier calls an array of objects line items, borrowing the accounting term for the rows of an invoice. When a trigger or action returns line items, Zapier keeps the list — but displays it in the flat field picker as parallel comma-joined fields. Our two-item order would surface roughly as:
Items Sku: MUG-11,TEE-MItems Quantity: 2,1The first value of each field belongs to the first line item, the second to the second, and so on — the record has been rotated, so to speak, from a list of rows into a set of columns. Some destination actions understand line items natively (an invoicing action that accepts line items will recreate the rows correctly); many do not, and mapping a line-item field into an ordinary text box gets you the literal comma-joined string. Untangling that — splitting line items into separate runs, or aggregating them into readable text — is Chapter 13's territory. For now the inspection-level point is: when you see suspicious commas in a Zapier field value, you are probably looking at a flattened array.
It also helps to know Zapier's unit of work: each new trigger record starts its own Zap run. If your form receives ten submissions, that is ten separate runs, each carrying one flat record through the steps. Zapier rarely asks you to think about "several records inside one run" — which is precisely why line items, the exception, feel awkward there.
You have two vantage points, and it matters which one you trust.
The first is the editor itself. When you build or edit a Zap, you test the trigger and Zapier fetches a few recent real records as sample data. Those samples are what populate the field picker's example values, and after you test any step you can review the exact data it received and produced right there in the editor's test panel.
The second — the ground truth for live behavior — is Zap history.
From Zapier's left-hand navigation, open the run history (labeled
Zap history or Zap runs, depending on
interface vintage; the entries themselves are Zap
runs). Click any run and you get a step-by-step replay; each
step offers a Data In and Data Out view. Be
precise about what those mean: Data In is the actual values
the step sent to the connected app — your mappings, filled in with that
run's real data — and Data Out is what the app returned.
Together they answer, field by field, exactly what happened on that
occasion. When someone says "the Zap did something weird on Tuesday,"
this is where Tuesday actually lives. Chapter 27 (Monitoring, History,
and Alerting) covers run history as an operational tool; here, just
register it as the place where "what did this step really receive?" gets
a definitive answer.
Watch out: Sample data in the Zapier editor is a snapshot, not a live feed. It reflects whatever records existed when you last tested, and a sample record can be unrepresentative — the one submission where someone skipped the phone field, say. If mappings look wrong or fields seem missing, re-test the step to pull fresh samples, and always confirm suspicions against
Data In/Data Outin the actual Zap run rather than the editor's remembered examples.
Make's model sits between Zapier's flattening and n8n's raw JSON, and its central noun is the bundle. A bundle is one packet of structured data — one record — moving through a scenario (Make's word for a workflow). The crucial design decision is this: a module can output multiple bundles, and every following module runs once per bundle it receives, automatically.
Read that twice, because it is the single most important sentence about Make. If a "Search Rows" module finds seven matching rows, it emits seven bundles, and the next module executes seven times — no loop to build, no special handling to configure. This one-becomes-many behavior is called fan-out, and the word will recur. Make counts each of those executions as an operation, which is also its billing unit; the bubble that appears above each module after a run displays the operation count. Multiplicity is not an edge case in Make; it is the default physics of the platform. (What that does to your bill is Chapter 31's subject — Unit Economics.)
Within a bundle, Make preserves JSON's structure rather than
flattening it, and gives the shapes friendlier names. A nested object is
a collection: Priya's company appears in
Make as a collection containing name and size,
displayed as an expandable branch. An array is simply an
array, shown with numbered elements you can expand; an
array of objects is an array of collections. When you map data into a
later module (Chapter 12), Make's panel presents this same tree, so the
nesting you see during inspection is the nesting you navigate while
building.
Make has a second, more formal concept worth knowing by name: the data structure. A data structure is a saved, named description of a data shape — "this thing has a string called email, a number called quantity, an array of collections called items" — that certain modules use to understand incoming or outgoing data. You meet data structures mainly at Make's raw edges: the "Parse JSON" tool, custom webhooks (the inbound web addresses from Chapter 9 that other apps push data to), and similar modules that receive arbitrary text and need to be told (or shown) what shape it takes. Conveniently, Make can generate a data structure automatically from a pasted sample of the JSON, which turns a chore into a paste-and-click. The same concept also defines the shape of Make's built-in storage (the Data Stores of Chapter 14). You already brushed against these raw edges in Chapter 9 (Webhooks and Raw HTTP) and Chapter 10 (When the Catalog Falls Short); the data structure is simply their formal machinery, now with a proper name. They live in Make's left sidebar alongside your connections and webhooks, so you can create one once and reuse it across scenarios.
Make's inspection story is the most visual of the three. Run a scenario (the editor's run button executes it once, live), and each module that executed grows a small numbered bubble showing how many operations it performed. Click the bubble and an inspector panel opens listing every operation with its input bundles and output bundles — the exact structured data in and out, presented as that expandable tree of collections, arrays, and values. If a module ran seven times, you can step through all seven operations and compare what each received.
For past runs, open the scenario's History tab: each
historical execution can be opened into the same diagram-with-bubbles
view, so "what did this module receive last Tuesday?" is answered the
same way as "what did it receive just now." (Retention of that history
varies by plan — on lower tiers it is short, so do not treat it as an
archive.)
Tip: After a run, Make lets you save a module's input and output bundles as JSON — look for the download option in the bundle inspector panel. This is quietly one of the most useful features in the product: the saved file is perfect for generating a data structure, attaching to a support ticket, or studying a gnarly payload in a proper editor instead of squinting at an expandable tree. One caveat: very large values can be truncated in these views, so for huge payloads capture the data at its source instead.
Watch out: Because every module runs once per incoming bundle, a module that unexpectedly emits many bundles multiplies everything downstream — actions taken, emails sent, operations billed. When a Make scenario "went crazy and sent forty messages," the diagnosis almost always starts by clicking bubbles right to left until you find the module whose output bundle count jumped. Inspection first; the aggregation tools that tame fan-out are in Chapter 13.
n8n shows you the least costume and the most truth. Data between
nodes (n8n's word for steps) always travels in the same shape: an
array of items, where each item is a JSON object. Even
one record is an array of items — an array with one element. Under the
hood each item wraps its data under a json key (with an
optional parallel binary part for files — Chapter 15's
territory), so a node's output looks like this in the raw:
[
{ "json": { "name": "Priya Raman", "email": "priya@example.com" } },
{ "json": { "name": "Marcus Webb", "email": "marcus@example.com" } }
]Two items in, and — like Make — the default behavior is that the next
node processes each item in turn, as though it ran once per item.
Multiplicity is native here too. The difference from Make is
presentational and philosophical: n8n does not rename JSON's parts or
wrap them in a metaphor. Objects are objects, arrays are arrays, and the
expressions you will write in Chapter 12 reference them with
programming-style paths like {{ $json.company.name }}.
Nothing is flattened, nothing is hidden, and nothing is simplified on
your behalf.
This is why n8n "feels like programming" even when you never write a line of code: the platform's working vocabulary is JSON's vocabulary, and it assumes you can read the brackets. Having just learned to read them, you can.
n8n's editor is built around inspection — arguably better than either competitor's. Open any node and you get a three-part view: the node's input data in a panel on one side, its output on the other, with the node's settings in the middle. Execute the node (or the whole workflow) and both panels fill with the real items, side by side, so cause and effect sit in a single glance: this went in, that came out.
Each panel offers three ways to view the same items, switchable at the top of the panel:
Past executions have the same anatomy: open the workflow's
Executions list, click a past run, and n8n replays the
whole workflow read-only with every node's actual input and output
browsable in those same panels. The "what did this step really receive
on Tuesday?" question is answered identically to the live version.
Tip: A reliable n8n habit: Schema view to orient ("what fields do I have and how are they nested?"), Table view to scan across many items, JSON view to settle arguments. When a mapping misbehaves, the JSON view is the only one of the three that cannot mislead you, because it is not summarizing anything.
Concrete beats abstract, so let one payload walk through all three platforms. A small web shop sends this JSON whenever an order is placed — one order, a nested customer, and two line items:
{
"order_id": "A-1042",
"placed_at": "2026-05-04T16:22:09Z",
"customer": {
"name": "Priya Raman",
"email": "priya@example.com"
},
"items": [
{ "sku": "MUG-11", "description": "Ceramic mug", "quantity": 2, "unit_price": 14.0 },
{ "sku": "TEE-M", "description": "T-shirt (M)", "quantity": 1, "unit_price": 22.5 }
],
"total": 50.5
}In Zapier, the trigger's field picker shows a flat
list: Order Id, Placed At,
Customer Name, Customer Email,
Total — and the array surfaces as line-item fields such as
Items Sku (MUG-11,TEE-M) and
Items Quantity (2,1). The customer's nesting
has vanished into compound field names; the items' row-ness has been
rotated into comma-joined columns. Mapping the customer email into a
follow-up email step is a two-second dropdown pick. Doing something
sensible per line item is the part that will need Chapter
13.
In Make, the webhook module emits one bundle. Click
its bubble and the inspector shows order_id and
total as plain values, customer as a
collection you expand to reveal name and
email, and items as an array containing two
collections, each expandable to its four fields. Nothing was lost and
nothing was renamed. If a later step needs to run once per item — say,
checking stock per SKU — Make's iterator tool will turn that one bundle
into two (Chapter 13), and every module after it will run twice,
automatically.
In n8n, the webhook node outputs one item whose
json holds the payload verbatim. Schema view shows the
outline — customer with two children, items
with two numbered elements of four fields each. JSON view shows exactly
the block printed above, wrapped in the item structure. Table view shows
one row, with the nested parts abbreviated. A later expression can reach
anything directly: {{ $json.customer.email }}, or the first
SKU as {{ $json.items[0].sku }} (square brackets pick a
list element by position, counting from zero — a programming convention
n8n inherits without apology).
The terminology lines up like this — a data-model slice of the fuller Rosetta Stone in Chapter 4:
| Concept | Zapier | Make | n8n |
|---|---|---|---|
| The workflow itself | Zap | scenario | workflow |
| One step | step | module | node |
| One record in flight | the run's fields | bundle | item |
| Several records | separate Zap runs; or line items within one run | several bundles (downstream modules run per bundle) | several items (nodes process each item) |
| Nested object | flattened into compound field names | collection (expandable) | JSON object, shown as-is |
| List / array | line items (comma-joined in the picker) | array (of values or collections) | JSON array, shown as-is |
| Where truth lives after a run | Zap run's Data In / Data Out |
bundle inspector via module bubbles; History tab |
input/output panels; Executions list |
Most automation debugging collapses into that one question. A workflow "sent the wrong name" — did the step receive the wrong name, or receive the right one and mangle it? The answer decides whether you look upstream or at the step itself, and each platform can answer it definitively if you look in the right place:
Data In (and compare
Data Out). The editor's test panel answers the same
question for test runs, but for live incidents trust the run history,
not the editor's samples.History tab), click the module's bubble,
and read the input bundles — per operation, if it ran more than
once.Executions list (or run in the editor) and read the node's
input panel, switching to JSON view when precision
matters.Two habits make the inspection reliable. First, always distinguish test-time data from run-time data: all three editors show you data from tests and samples while you build, and all three keep separate records of what actually happened in production. The former is for construction; the latter is for truth. Second, inspect both sides of a suspect step — input and output — before forming a theory. A surprising number of "the app returned garbage" complaints turn out to be "the step received garbage," and thirty seconds of looking settles it. Chapter 26 (Testing, Debugging, and Launch) builds a full workflow-verification practice on these foundations, and Chapter 28 (Diagnosing Failures) turns them into a systematic fault-finding method.
Tip: When you file a support ticket or ask a colleague for help, include the step's actual input and output — Zapier's
Data In/Data Out, a downloaded Make bundle, or copied n8n JSON — with any private details redacted. A question with real payloads attached gets answered in one round trip; a question described from memory rarely does.
Step back and the three designs resolve into three answers to one question: what should the platform do about the gap between JSON's true shape and a human's tolerance for brackets?
Zapier answers: hide the shape. Flattening buys the gentlest possible on-ramp — the field picker with sample values gives the friendliest first hour of the three platforms — by spending structure. One record with modest nesting costs you nothing. But arrays get rotated into comma-joined line items, deeply nested payloads sprout long awkward field names, and "do this for each of the seven results" cuts against the one-record-per-run grain. Zapier is easiest exactly as long as your data stays flat, and the difficulty arrives suddenly when it does not.
Make answers: package the shape. Bundles keep JSON's structure but wrap it in a visual, explorable container, and the run-once-per-bundle rule makes multiplicity feel effortless — search returns seven rows, downstream just happens seven times. The costs are conceptual and financial: you must internalize fan-out to predict what a scenario will do (and what it will cost in operations), and the bundle/collection/array vocabulary is one more layer of names over the JSON you will still occasionally meet raw at the webhook edges.
n8n answers: teach you the shape. Items are JSON, the panels show everything, and expressions reach anything. Nothing surprising can hide, because nothing is disguised — which is precisely why developers relax inside n8n and newcomers tense up. The learning cost is real but front-loaded: it is essentially the cost of this chapter. Once brackets read as easily as sentences, n8n's model is the simplest of the three, because it is the only one with nothing between you and the data.
As a rough guide to comfort by task:
| Situation | Zapier | Make | n8n |
|---|---|---|---|
| One flat record per event (form fill, new contact) | Excellent — this is the home case | Comfortable | Comfortable |
| Nested record (order with customer object) | Workable; flattened names get clumsy | Natural — expand the collection | Natural — it is just JSON |
| Many records per event (search results, line items) | The pain point; line items need Ch. 13 techniques | Natural — bundles fan out automatically | Natural — items flow through |
| Raw or unusual JSON from a webhook | Hardest to see clearly | Good, via data structures | Best — you see exactly what arrived |
| Predicting what a run will do without running it | Easy (one record, one path) | Requires thinking in fan-out | Requires thinking in items |
None of these judgments is a verdict on overall platform quality — Chapter 35 (The Decision Framework) weighs data-model fit alongside everything else. But if your automations will regularly carry lists, nested payloads, or raw webhook data, the data model deserves real weight in the decision, because it is the difference you will feel every single day you build.
You can now read JSON, name each platform's shapes, and find any step's real input and output. The next three chapters put that literacy to work: Chapter 12 (Mapping and Transforming Fields) changes the values as they flow, Chapter 13 (Lists, Loops, and Aggregation) tames the many-at-once cases this chapter only inspected, and Chapter 14 (Storing Data Inside the Platform) gives data somewhere to live between runs. Everything in them stands on the ability you just built — looking at a step and knowing, precisely and without guessing, what shape the data is in right now.
A workflow is only as good as the values that move through it. The
trigger hands you a raw record — a form submission, a new CRM contact,
an incoming order — and every step after that needs some piece of it:
the email address, the deal amount, the signup date. Getting a value
from one step into another is called mapping. Changing
its shape along the way — trimming stray spaces, fixing SHOUTED names,
turning "$1,299.00" into a number a spreadsheet can add up
— is called transformation.
Every platform in this guide does both, but in three very different styles. Zapier gives you a friendly dropdown and a family of dedicated cleanup steps. Make expects you to type small functions directly into the field you are filling. n8n hands you a full expression language behind double curly braces. This chapter walks through all three pickers, then all three transformation toolkits, and then works the problems every practitioner meets in the first month: text cleanup, name casing, number and currency hygiene, lookup tables, and the date-and-timezone trap that has ruined more launch days than any other single topic in this book.
One boundary before we start: this chapter is about single values inside one record — one name, one price, one date. The moment you have a list of things (five line items on one order, an array of tags), you are in Chapter 13 (Lists, Loops, and Aggregation). And when a transformation gets too gnarly for the built-in tools, the escape hatch is a code step — that is Chapter 10 (When the Catalog Falls Short: Custom Connectors and Code Steps).
The idea is identical everywhere: when you configure a step, its
input fields can hold either literal text you type (the same
every run) or references to data produced by earlier steps
(different every run). A reference says "whatever the trigger's
email field contains this time, put it here." The platforms
differ mainly in how you insert that reference and what you see
while you do it.
All three lean on sample data — a real record pulled in while you build, so the picker can show you actual values instead of abstract field names. This is why Chapter 5 (Hands On) had you test the trigger before building anything else: without a sample, the pickers are flying blind.
Tip: When a picker shows you stale or missing fields, the fix is almost always to refresh the sample: re-test the trigger (Zapier), right-click the trigger module and re-run it (Make), or execute the previous node again (n8n). Build against a recent, realistic sample — ideally one with the messiest data you expect.
Click into any input field in the Zap editor and a dropdown appears, organized by step: your trigger first, then each action after it. Each entry shows the field's name and the value from your test record — "First Name: martha" — which makes choosing the right field mostly self-evident. There is a search box, because real apps routinely expose dozens or hundreds of fields. Selecting an entry drops a small labeled token — commonly called a pill — into the field.
You can freely mix pills and typed text in one box:
Hello {First Name}, thanks for your order! is one literal
sentence with one mapped value inside it. This mixing is Zapier's quiet
superpower for email bodies and Slack messages.
What Zapier does not offer inside the picker is any way to change the value. The pill delivers the field exactly as the upstream app produced it. All reshaping happens in separate Formatter steps, which we will meet shortly.
In Make, clicking into a module's field opens the mapping panel — a floating palette with two personalities. One part is a tree of every field emitted by earlier modules, shown as colored pills you click to insert. The other part is a row of tabs holding Make's function library: general utilities, math, text, date and time, and array functions, each insertable with a click or typed by hand.
The crucial difference from Zapier: Make lets you wrap pills
in functions right there in the field. upper(1.name) — the
upper() function applied to module 1's name
field — is a perfectly normal thing to type into the middle of any input
box. The pill and the function tokens render as chips, so a transformed
mapping still reads visually rather than as raw code.
Two Make conventions surprise newcomers, so learn them now. First,
function arguments are separated by semicolons, not
commas — replace(1.name; "-"; " ") — because
commas are decimal separators in many locales. Second, when you
eventually meet arrays, Make counts from 1, not 0.
Neither is wrong; both are different from almost every programming
language you may half-remember.
Open any node in n8n and the editor shows three panes: input data on
the left, the node's settings in the middle, output on the right. The
input pane offers a Schema view — a tidy tree of field
names — and you map by dragging a field from that tree into a
settings box. n8n writes the reference for you, in its expression
syntax: {{ $json.email }}.
Every parameter box in n8n has a small toggle between
Fixed (literal text) and Expression
(computed). In expression mode, anything between {{ and
}} is evaluated as JavaScript against the incoming data —
$json means "the JSON of the item currently flowing through
this node," and $('Webhook').item.json.email reaches back
to a specific earlier node by name. If that sentence made your eyes
glaze, hold on: you can go a long way on drag-and-drop alone, and the
expression editor shows a live preview of the computed
result using your sample data, so you always see what a run would
actually produce.
Mappings are references, and references can dangle. If someone renames a question on the upstream form, changes a custom field in the CRM, or a webhook payload arrives without a field it usually has, the mapping either resolves to nothing or to the wrong thing — and the platforms are quietly different about which identifier they cling to. Zapier pills bind to the upstream field's internal ID, which survives some renames and not others. Make pills bind to the raw field names in the module's output. n8n expressions reference literal JSON keys, so any key change upstream breaks them.
Watch out: A broken mapping usually fails silently — the step runs and simply inserts an empty value. "Hi , thanks for your order!" in a customer's inbox is the classic symptom. Chapter 28 (Diagnosing Failures) covers how to spot this in run history; Chapter 19 (Error Handling by Design) covers guarding against it. The cheap insurance here and now is a default value on any field that matters, which every toolkit below can supply.
Here is the philosophical fork in the road. All three platforms can perform essentially the same fifty-odd single-value transformations. They disagree about where the transformation lives.
| Zapier | Make | n8n | |
|---|---|---|---|
| Where transforms live | Separate Formatter steps between actions | Functions typed inline in any mapping field | Expressions inline in any field, plus the Edit Fields node |
| Style | Pick an operation from menus, fill in blanks | Small spreadsheet-like function calls | JavaScript snippets in {{ }} |
| Visibility in run history | Excellent — each transform is its own step with input and output | Inline — inspect the module's input bundle to see the computed value | Inline — inspect the node's resolved parameters and output |
| Failure mode at scale | Step sprawl: long chains of Formatter steps | Dense nested one-liners that are hard to read | Expressions scattered across many nodes |
| Learning curve | Lowest | Middle | Highest, and the most powerful ceiling |
Zapier's answer is an in-house app called Formatter by Zapier. You add it to the Zap like any other action, choose one of four event families — Text, Numbers, Date / Time, or Utilities — then choose one specific transform from a menu and fill in the blanks. The transformed value comes out as that step's output, ready to map into later steps.
The Text family covers the everyday needs: trim whitespace, change case, split text, find and replace, truncate, extract an email address or URL from a blob of prose, set a default value when the input is empty. Numbers handles number and currency formatting, phone number formatting, basic math, and a spreadsheet-style formula option for the moments you miss Excel. Date / Time parses and reformats dates and does date arithmetic. Utilities holds the lookup table (which we will use below) and the line-item tools that belong to Chapter 13.
The great virtue of this design is legibility. Each transformation is a named step; in run history you can see exactly what went in and what came out, one transform at a time. The great cost is sprawl: each Formatter step performs one operation on one value, so "trim it, titlecase it, and provide a default" is three steps before you have touched a real app. A Zap that cleans five fields can easily be fifteen steps long, and each is one more thing to scroll past.
The sprawl costs you legibility, not money: under Zapier's current pricing, built-in steps such as Formatter, Filter, and Paths do not count against your plan's task allowance. That rule has shifted before, so confirm it on the pricing page — and see Chapter 31 (Unit Economics) for how internal steps figure into cost on each platform.
Make's answer is the function library baked into every mapping field.
There is no separate cleanup step; you transform the value in the exact
place you use it. capitalize(trim(1.first_name)) typed into
an email module's greeting field trims and capitalizes in one motion, at
zero additional cost, because Make bills by module executions
(operations) and inline functions are not modules.
The library reads like a friendly spreadsheet dialect:
trim(), upper(), lower(),
capitalize(), startcase(),
replace(), split(), join(),
formatNumber(), parseNumber(),
formatDate(), parseDate(),
switch(), ifempty(), and a hundred or so
relatives. Functions nest: the output of one becomes the argument of
another, evaluated inside-out.
The virtue is economy — no extra steps, no extra operations,
transformation exactly where you need it. The cost is readability at
density. A field containing three nested functions is fine; a field
containing a switch() inside a replace()
inside an if() wrapped around two parseDate()
calls is a puzzle box your successor will curse. When a mapping field
starts to look like that, Make's own escape valve is a
Tools > Set variable module: compute the value once in a
dedicated module, name it something honest, and map the named result
everywhere else.
n8n's answer is the expression. Any field can hold {{ }}
blocks containing JavaScript, which means the entire JavaScript standard
library for strings, numbers, and math is available inline:
{{ $json.email.trim().toLowerCase() }} is a complete,
working transformation. On top of raw JavaScript, n8n adds its own
convenience helpers — methods like toTitleCase() and
extractEmail() that appear on values inside expressions —
and ships the Luxon date library for time handling (more on that in the
date section). The expression editor autocompletes as you type and
previews the computed result live against your sample data.
For tidiness, n8n offers the Edit Fields node (long
known as the Set node, and often labeled
Edit Fields (Set) in the node picker). It does nothing but
define fields: you list name–value pairs, where each value can be fixed
or an expression, and choose whether to keep or discard the other
incoming fields. This makes it the natural "normalization station": put
one Edit Fields node right after your trigger, compute clean versions of
every field you care about — email,
first_name, amount, due_date —
and let every downstream node map from those instead of from the raw
trigger payload.
The virtue is power with structure: anything JavaScript can
do to a value, an expression can do, and Edit Fields keeps it organized.
The cost is that the ceiling and the floor are both higher — the first
{{ $json.x }} is a small hurdle for a non-programmer, and
nothing stops a team from smearing thirty ad-hoc expressions across a
workflow. The Edit Fields habit is the discipline that prevents
that.
Tip: Whatever the platform, normalize early. One Formatter block, one
Set variablemodule, or one Edit Fields node near the top of the workflow — producing clean, canonically named values — beats scattering the same trim-and-titlecase logic across nine downstream fields. When the upstream app changes its output, you fix one place.
Now the practitioner's greatest hits. For each problem: what it is, why it bites, and how each toolkit handles it. A compact Rosetta table first, then the details worth knowing.
| Task | Zapier | Make | n8n |
|---|---|---|---|
| Trim whitespace | Formatter > Text > Trim Whitespace |
trim(1.name) |
{{ $json.name.trim() }} |
| Uppercase / lowercase | Formatter > Text > Uppercase /
Lowercase |
upper() / lower() |
.toUpperCase() / .toLowerCase() |
| Capitalize each word | Formatter > Text > Capitalize |
startcase(1.name) |
{{ $json.name.toTitleCase() }} |
| Split and take a piece | Formatter > Text > Split Text |
get(split(1.name; " "); 1) |
{{ $json.name.split(" ")[0] }} |
| Join two values | map both pills into one field | type both pills into one field | {{ $json.first + " " + $json.last }} |
| Default when empty | Formatter > Text > Default Value |
ifempty(1.phone; "unknown") |
`{{ $json.phone |
| Find and replace | Formatter > Text > Replace |
replace(1.sku; "-"; "") |
.replaceAll("-", "") |
| Parse messy number | Formatter > Numbers > Format Number |
parseNumber(1.price; ".") |
{{ parseFloat($json.price.replace(/[$,]/g, "")) }} |
| Lookup table | Formatter > Utilities > Lookup Table |
switch(1.cc; "US"; "United States"; …) |
object lookup in an expression |
Two fine-print notes on the n8n column, because they are standard
JavaScript behavior and bite everyone once: .replace()
given plain text swaps only the first occurrence — use
.replaceAll() (or a regex — a regular expression,
the pattern-matching mini-language — with its g "global"
flag) to catch them all — and the ?? operator only supplies
a default when the value is null or absent, so prefer
|| when an empty string should also trigger the
fallback.
Trim removes the invisible spaces, tabs, and stray
line breaks that ride along at the start or end of user-entered text. It
looks cosmetic and is not: "martha@example.com " with a
trailing space is, to most APIs, a different and invalid email address.
Deduplication, lookups, and equality filters all fail on untrimmed text.
The habit worth forming is brutal and simple: trim everything that a
human ever typed, as the first transformation, before any
other.
Split cuts one string into pieces at a separator.
The single-value use case — the subject of this chapter — is "split and
keep one piece": take "Martha Nguyen" and keep the first
word, or take "order-4412-US" and keep the middle chunk.
Zapier's Split Text transform asks for the separator and
which segment you want (first, second, last, second-to-last).
Make's split() produces an array, so you wrap it in
get() to pluck one element — remembering that Make counts
from 1, so get(split(1.full_name; " "); 1) is the
first word. n8n uses JavaScript's .split(), which
counts from 0: [0] is the first piece, and the handy
.at(-1) grabs the last. The moment you want to keep
all the pieces and act on each, you have left this chapter for
Chapter 13.
Join, in the single-record sense, is just
concatenation — sticking values end to end: building
"Martha Nguyen" from a first and a
last field, or composing a one-line address from four
components. In Zapier and Make you rarely need a function at all — type
the pills next to each other with a space between them. In n8n, either
write the fields adjacently in a fixed-plus-expression field or
concatenate inside one expression. The only real trap is missing
middles: first + " " + middle + " " + last produces
"Martha Nguyen" — two spaces — when middle is
empty. Apply your default-value tool, or accept the cosmetic
wrinkle.
Users type their names as MARTHA NGUYEN,
martha nguyen, and mArThA; your CRM and your
email greetings want Martha Nguyen. Three distinct
behaviors exist, and the platforms label them inconsistently — so learn
the behaviors, not the names. The first uppercases only the first letter
of the whole string ("martha nguyen" →
"Martha nguyen"): that is Make's capitalize().
The second uppercases the first letter of every word
("martha nguyen" → "Martha Nguyen"): Make's
startcase() and — despite what its name suggests — Zapier's
Capitalize, which works word by word. The third capitalizes
each significant word but deliberately leaves minor words like
"a," "of," and "the" lowercase, the way a headline does: that is
Zapier's separate Titlecase and — despite its name
— n8n's toTitleCase(), which turns
"quick a brown fox" into "Quick a Brown Fox",
not "Quick A Brown Fox". For plain two-word names the
second and third behave identically; they diverge only when a minor word
appears, as in a company name or a full title. For person names, use one
of the each-word transforms — and test whichever one you picked on a
two-word sample before trusting the label. One n8n quirk belongs here:
toTitleCase() deliberately leaves letters that are
already uppercase untouched — the same behavior that keeps
iPhone and acronyms intact — so it does nothing to an
all-caps MARTHA NGUYEN, returning it unchanged. To re-case
shouted input in n8n, lowercase first:
{{ $json.name.toLowerCase().toTitleCase() }}.
Know the limits before you automate this on real humans. Naive
word-by-word casing mangles names that carry their own internal capitals
or particles: MCDONALD becomes Mcdonald, not
McDonald; VAN DER BERG becomes
Van Der Berg, which the Van der Bergs may not love;
O'BRIEN may or may not survive depending on how the
function treats apostrophes. No built-in transform on any of the three
platforms handles these cultural edge cases correctly, because no simple
rule can. The pragmatic policy: re-case only when the input is clearly
broken (all-caps or all-lower), pass through anything with mixed case
untouched — an easy condition to express in Make or n8n, and achievable
in Zapier with a filter or a code step (Chapter 10). Your names will be
right for the 95%, and unmangled for the rest.
Chapter 11 (Three Data Models) explained the difference between the
number 1299.5 and the text
"1,299.50" — same idea to a human, different species to a
machine. Field mapping is where that distinction stops being academic.
Payment and form apps routinely emit amounts as decorated text —
"$1,299.50", "1.299,50 €" — and if you map
that straight into a spreadsheet sum, a CRM revenue field, or a
comparison filter ("is amount greater than 1000?"), you get wrong
answers or hard failures.
The cleanup is always two moves: parse the text into
a genuine number, then, only at the point of display,
format it back into pretty text. Zapier's
Formatter > Numbers > Format Number reshapes a number
between grouping conventions — you tell it which decimal mark the input
uses — though a stray currency symbol may need a
Formatter > Text > Replace step to strip it first;
Format Currency goes the other way, dressing a plain number
in a chosen currency style. Make separates the moves explicitly:
parseNumber(1.price; ".") — where you tell it
which character is the decimal separator — and
formatNumber() to dress the result. n8n leans on
JavaScript: strip the decoration, then convert —
{{ parseFloat($json.price.replace(/[$,]/g, "")) }} — and
format for display with .toFixed(2) or similar.
The trap inside the trap is locale — the regional
convention for writing numbers. In the US and UK, 1,299.50
uses a comma to group thousands and a period for decimals. In much of
Europe, the same value is written 1.299,50. A
parser assuming the wrong convention does not error; it happily returns
a number a thousand times too large or small. If your inputs can come
from more than one region, you must know (or detect) which convention
each source uses — this is precisely why Make's parse functions make you
name the decimal separator, and it is a strong argument for normalizing
amounts at the source app when you can.
One more habit: keep the parsed number as a number for as long as
possible. Formatting to "$1,299.50" early and then trying
to do math on it later just re-creates the original problem
downstream.
Constantly, one system speaks in codes and the next wants words: the
form captures US, the CRM wants United States;
the store emits plan code pro_m, the invoice should say
Professional (Monthly); a status of 3 should
read Shipped. A lookup table is the tool:
a small dictionary of key–value pairs, plus a default for keys you did
not anticipate.
Zapier makes this a first-class transform:
Formatter > Utilities > Lookup Table presents a
two-column key/value editor and a fallback field, and the mapped input
picks the matching row. It is the most beginner-friendly lookup in the
industry — use it freely.
Make expresses the same idea with the switch() function,
which takes the input followed by pairs of match-and-result, with an
optional final argument as the default:
switch(1.country; "US"; "United States"; "CA"; "Canada"; "GB"; "United Kingdom"; 1.country)
— here the fallback is the original code, so unknown countries pass
through visibly instead of vanishing.
n8n does it with a plain JavaScript object lookup inside an expression:
{{ ({"US": "United States", "CA": "Canada", "GB": "United Kingdom"})[$json.country] ?? $json.country }}
The ?? supplies the fallback. If the table grows past a
dozen rows or needs non-technical editing, it has outgrown an inline
expression — promote it to a data table inside the platform (Chapter 14,
Storing Data Inside the Platform) or a small spreadsheet the workflow
reads.
Watch out: Always define the fallback, and make it loud. A lookup that silently returns empty for an unknown key produces blank invoices and unroutable records weeks later, when someone adds a new plan code and forgets your table exists. Falling back to the raw input — or to a sentinel (a deliberately conspicuous marker value) like
UNMAPPED-<code>you can search for in run history — turns a silent gap into a visible one.
Dates deserve their own section because they are the one data type where every platform, every app, and every country disagrees, and where a value can be correct and wrong at the same time.
A date travels between apps as text, and the text comes in dialects.
ISO 8601 is the international standard and the only
dialect you should ever voluntarily produce: 2026-07-17 for
a date, 2026-07-17T14:30:00Z for a moment in time, where
T separates date from time and the trailing Z
means UTC — Coordinated Universal Time, the global reference clock. A
timestamp can carry an explicit offset instead, like
2026-07-17T14:30:00-06:00, meaning "six hours behind UTC."
Then there is the epoch timestamp (also called Unix
time): a plain number counting the seconds — or, in some systems,
milliseconds — since January 1, 1970 UTC. If a date arrives as
a ten-digit number, it is seconds; thirteen digits, milliseconds; feed
one where the other is expected and your dates land in 1970 or tens of
thousands of years in the future.
And then there are the regional dialects that cause real damage:
03/04/2026 is March 4th to an American system and April 3rd
to a European one. Nothing in the string tells you which. Any workflow
that ingests slash-formatted dates from humans or regional apps
must be told the convention explicitly — every parsing tool
below has a "from format" input for exactly this reason. Never let a
parser guess.
2026-07-17 can mean two different daysHere is the trap the section title promised. 2026-07-17
names a calendar day, not a moment. But software pipelines
mostly traffic in moments — so somewhere along the line, a system
quietly promotes your day to a moment by bolting on a time (midnight)
and a timezone (its own default, often UTC). 2026-07-17
becomes 2026-07-17T00:00:00Z: midnight UTC.
Now that moment travels to a step, an app, or a human display configured for, say, US Central time — five hours behind UTC in July, under daylight saving. Midnight UTC on July 17th is 7:00 PM on July 16th in Chicago. The calendar day the tool displays is the 16th. Your deadline, birthday, or publish date has moved back a day, with no error anywhere, because every component behaved "correctly."
The same slip runs in reverse: a user in Sydney picks July 17th in a date picker; the app stores it as midnight Sydney time, which is still July 16th in UTC; a UTC-configured workflow files it under the 16th. Different engines even disagree about the promotion rule itself — plain JavaScript treats a date-only ISO string as UTC midnight, while the Luxon library n8n ships treats it as midnight in the configured local zone — which is why "it worked in testing" is no defense.
The cure is a policy, not a function: for calendar dates (birthdays, due dates, holidays), keep them as date-only strings end to end and never convert them through a timezone. For moments (a meeting start, a payment timestamp), always carry the timezone explicitly and convert only at display time. Most date bugs in automation are a violation of one of those two sentences.
Every date manipulation on every platform is the same two moves, and doing them explicitly is what keeps you safe. Parse: turn the incoming text into the platform's internal date value, stating the input's format and, if it lacks one, its timezone. Format: turn the internal value back into text, stating the output format and output timezone. State all four things every time — input format, input zone, output format, output zone — even when a default would happen to work today.
In Zapier, this is one step:
Formatter by Zapier > Date / Time > Format. It offers
a To Format, a To Timezone, and — the fields beginners skip and should
not — a From Format and From Timezone that tell Zapier how to read the
input. In Make, it is two functions:
parseDate(1.due; "MM/DD/YYYY"; "America/Chicago") to read,
formatDate(…; "YYYY-MM-DD"; "UTC") to write; both accept
the timezone as the optional final argument. In n8n, expressions use
Luxon: {{ $json.due.toDateTime() }} covers well-formed
input,
DateTime.fromFormat($json.due, "MM/dd/yyyy", {zone: "America/Chicago"})
handles the dialects, and .toFormat("yyyy-MM-dd") writes
the output. Date arithmetic — add three business days, compare
two dates — uses the same internal values: Zapier's
Add/Subtract Time and Compare Dates
transforms, Make's addDays() family, Luxon's
.plus() and .diff() in n8n.
Timezone names, everywhere, should be the IANA
identifiers — region/city names like
America/Chicago and Europe/Berlin, drawn from
the shared timezone database nearly all software uses — not
abbreviations like CST, which are ambiguous (three
different zones answer to "CST") and ignorant of daylight saving.
The letters that describe a date format are themselves a compatibility trap, because Make and Zapier follow one convention while n8n's Luxon follows another:
| Meaning | Make / Zapier style | n8n (Luxon) |
|---|---|---|
| Four-digit year | YYYY |
yyyy |
| Month (01–12) | MM |
MM |
| Day of month (01–31) | DD |
dd |
| Hour, 24-hour clock | HH |
HH |
| Minutes | mm |
mm |
Watch out: Copying a format string between platforms is not safe.
YYYY-MM-DDpasted into an n8n Luxon.toFormat()does not produce a calendar date — in Luxon, capitalDDmeans something entirely different (a localized date style), and the output will look plausible enough to slip past a quick glance. When a date format misbehaves after a migration, check token case first.
When you do not state a zone, each platform substitutes its configured default — so know where yours lives. Zapier keeps a timezone on your account profile (in the account settings), and the editor allows overriding it for an individual Zap in that Zap's settings. Make carries a timezone on the organization, and separately on your user profile — and the two govern different things (computation versus what the web interface displays to you), a mismatch that has convinced many builders their data was wrong when only the display was. n8n sets a default for the whole instance and lets each workflow override it in the workflow's settings panel. Check yours once, deliberately, before the first date-bearing workflow goes live — and note that the timezone governing schedule triggers ("run every day at 9 AM") is its own topic, covered with triggers in Chapter 8 (Triggers: How Workflows Start).
Four closing disciplines, cheaper on day one than on day ninety.
Normalize once, early. The Edit Fields node, the
Set variable module, the block of Formatter steps right
after the trigger — one station that emits clean, canonically named,
correctly typed values, so the other nine steps map from a stable
interface instead of a raw payload.
Test with hostile data. Your sample record is polite. Before launch, run the workflow with a name in all caps, a trailing space on the email, an empty optional field, a European-formatted amount, and a date near midnight at a month boundary. Chapter 26 (Testing, Debugging, and Launch) turns this into a checklist.
Prefer legible over clever. A nested one-liner you cannot read in six months is a liability. Break it into two named fields, or two Formatter steps, or — when it genuinely needs logic — promote it honestly to a code step (Chapter 10) rather than contorting the mapping tools past their comfort.
Write down the shape. One comment or note per workflow stating the canonical field names, the number locale you assume, and the timezone policy. It is the cheapest documentation you will ever produce, and the first thing your successor — or you, in six months — will thank you for.
Transformation is where workflows stop being plumbing and start being judgment: every trim, lookup, and timezone decision encodes what "clean" means for your business. The next two chapters extend the same thinking from single values to the harder shapes — lists and collections in Chapter 13, and the platforms' own storage in Chapter 14.
Sooner or later, every automation hits the same wall. An order arrives carrying five products. A form submission includes three file attachments. Your Monday digest needs to summarize forty support tickets in a single email. Up to that point, everything you built handled one thing at a time — one lead, one row, one message — and all three platforms made that feel easy. The moment a workflow has to handle several things at once, the platforms stop resembling each other, and this is the single deepest conceptual gap in the entire n8n–Make–Zapier landscape.
Chapter 11 (Three Data Models) introduced the shapes data takes on each platform: Zapier's flat record, Make's bundle, n8n's item. This chapter is about plurality — what happens when there is more than one of them, or when one of them contains a list inside it. Get this chapter into your bones and a whole class of "mysterious" bugs — duplicated rows, comma-mashed values, steps that ran fifty times when you expected one — becomes boring and diagnosable.
Two terms before we start. A list (called an
array in programming) is an ordered collection of
values: ["red", "green", "blue"] is a list of three
strings, and an order's products are a list of product records. A
loop is the act of doing the same work once for each
element of a list. Everything in this chapter is a variation on those
two ideas.
Every many-at-once problem decomposes into at most three moves:
Not every workflow needs all three. Sometimes you only split (order in, one inventory update per product out). Sometimes you only collapse (many rows in, one summary out). The classic full pattern — split, process each, collapse — is what Make users call iterate then aggregate, and it exists on all three platforms under different names.
The vocabulary is the first stumbling block, so here is the map:
| Concept | Zapier | Make | n8n |
|---|---|---|---|
| One unit of data flowing through | A run's single record | Bundle | Item |
| A list inside a record | Line items | Array | Array field (JSON) |
| Split one into many | Looping by Zapier; some actions accept line items natively | Iterator module | Split Out node (or the run-per-item default) |
| Collapse many into one | Formatter line-item utilities; Digest by Zapier (across runs) | Aggregator modules (Array, Text, Numeric, and specialized) | Aggregate node |
| Explicit batch loop | Looping by Zapier | No direct equivalent — Iterator fans out all at once | Loop Over Items node |
| Loops implicitly? | Almost never | Yes — every module runs once per incoming bundle | Yes — most nodes run once per incoming item |
The last row is the one to memorize. Zapier treats a run as one record and makes you work to express plurality. Make and n8n treat plurality as the native condition and make you work to stop looping. Most cross-platform confusion is someone carrying one platform's default in their head while building on another.
Zapier's mental model, per Chapter 3 (Mental Models: How a Run Actually Happens), is that a trigger fires once per record and the whole Zap runs top to bottom for that single record. Ten new spreadsheet rows means ten separate Zap runs, each blissfully unaware of the others. That design keeps simple Zaps simple — and it means plurality inside one run had to be bolted on. The bolt is called line items.
A line item set is Zapier's version of a list of records: parallel lists of values that belong together, named after the line items on an invoice. When a Shopify-style order trigger fires, you get order-level fields (customer, total, date) as ordinary values, plus line-item fields (product name, quantity, price) where each field secretly holds several values — one per product.
The awkwardness, and it is genuinely notorious, shows up when you map line-item fields into a following step (mapping itself is Chapter 12's territory):
Widget,Gadget,Sprocket,Doohickey,Gizmo crammed into one
cell.Nothing errors. Nothing warns. The run shows a green check. You discover it three weeks later when someone opens the spreadsheet. Comma-mashed values in a downstream system are the signature symptom of line items flattened by a non-line-item-aware field.
Tip: Before trusting any Zap that touches line items, run one test with a record containing at least two elements and inspect the actual output in the destination app — not just Zapier's run history. A single-element test hides flattening completely, because a list of one flattens into itself.
Zapier's Formatter app has a Utilities action group that
is the workbench for line items:
These three, plus filters, cover a surprising share of real cases without any looping at all. When you truly need to do work per element, you need the loop.
Looping by Zapier is a built-in app whose action creates a loop from a line-item set, a delimited text value, or a range of numbers. Every step you place after the loop step runs once per iteration — if the loop produces five iterations, a following "Send email" step sends five emails.
Its quirks matter more than its features:
Zapier has one aggregation trick the other platforms express differently: Digest by Zapier. Because ten new leads are ten separate runs, no single run can see all ten — so Digest gives you a named accumulator that each run appends a line to, and a release step that empties it on a schedule or on demand. That is how you build "every Friday, one email listing the week's signups" in a one-record-per-run world. It is aggregation across runs rather than within one, and it quietly stores state inside the platform — the broader topic of Chapter 14 (Storing Data Inside the Platform).
Make's unit of flow is the bundle — one packet of structured data moving along the line between modules. The rule that makes Make click is: every module runs once per bundle it receives. If a search module finds twelve matching contacts, it outputs twelve bundles, and every subsequent module runs twelve times without you asking. Make loops implicitly, all the time. Many newcomers build a scenario, watch it execute, and are startled by the little bubble on each module showing it ran a dozen times.
So in Make you rarely ask "how do I loop?" — looping is ambient. The real questions are "how do I turn a list inside a bundle into bundles?" and "how do I make it stop being many bundles?" Those are the Iterator and the Aggregator.
The Iterator (found among Make's flow-control tools) takes an array field from an incoming bundle and emits one bundle per element. An order bundle containing an array of five products goes in; five product bundles come out; everything downstream now runs five times. That is the entire module — one setting, which array to explode.
You often do not need it: app modules that return multiple records already output multiple bundles, pre-iterated. You need the Iterator specifically when the plurality is nested inside one bundle — line items inside an order, attachments inside an email, rows inside a parsed CSV file (comma-separated values, the plain-text spreadsheet format).
Do not confuse it with its flow-control neighbor the Repeater, which emits a fixed number of near-identical bundles — it repeats N times; it does not walk a list. Reaching for Repeater when you mean Iterator is a classic early-Make mistake.
The Aggregator family does the reverse — consumes a stream of bundles and emits a single bundle:
Group by option can produce one aggregate bundle per
distinct key (one summary per customer, say) instead of one
overall.Every aggregator has one setting that outranks all others: Source Module. It answers the question "aggregate everything since where?" The aggregator collects all bundles that trace back to one bundle emitted by the source module, then closes and emits its single result. Point it at the Iterator and you collapse exactly what that Iterator exploded — five product bundles back into one order summary. Point it at the wrong module and you get aggregates scoped too wide or too narrow, which is among the most common Make bugs in the wild.
Watch out: If your aggregator emits one giant bundle when you expected several, or several when you expected one, check
Source Modulebefore anything else. The aggregator's scope is defined by that setting, not by its position on the canvas.
The canonical Make shape sandwiches per-element work between the two:
Source Module
set to the Iterator — one bundle again, now enriched.Watch out: Every module between an Iterator and its Aggregator runs once per element, and each of those executions is an operation — Make's billing unit. Three modules inside the sandwich, two hundred elements: six hundred operations from one trigger event. The sandwich is powerful precisely because it multiplies work; remember that it multiplies cost by the same factor.
One economy worth knowing: for simple extractions, Make's
map() function (usable inside any field mapping) can pull a
list of values out of an array — "all the product names" — without
iterating at all, costing zero extra operations. If you find yourself
iterating only to aggregate one field straight back, a
map() expression plus a join() often replaces
the whole sandwich.
n8n goes one step further than Make: the connection between two nodes doesn't carry one item — it carries a list of items, always. Even a single record travels as a list of one. And most nodes obey one rule: run once per incoming item. Fifty items in, fifty executions, fifty items out (or more, or fewer — nodes can multiply or filter). Like Make, n8n loops implicitly; unlike Make, the list itself is visible everywhere. Open any node in the editor after a test run and the input and output panels show you the items as a table or as JSON (JavaScript Object Notation, the standard text format for structured data), with a count at the top.
That count is your best friend. Most n8n list bugs are solved by looking at how many items entered a node and how many left, then finding the node where the number stopped matching your expectation.
Tip: When an n8n workflow misbehaves, walk the executed nodes left to right reading only the item counts —
5 items,5 items,1 item, wait, why one? The node where the count surprises you is where the bug lives. You will fix more bugs with this habit than with any amount of expression debugging.
Because items are native, n8n's split and collapse tools are refreshingly plain:
For text digests, the usual pattern is Aggregate first, then build the message with an expression that joins the collected array — or use a Code node, below.
If nodes already run per item, what is the Loop Over Items node (formerly, and still internally, called Split in Batches) actually for? Three things:
The node has two outputs: a loop output that feeds the body of the loop, and a done output that fires once when every batch has been processed. The body's last node must connect back to Loop Over Items' input — that return wire is what makes it a loop, and forgetting it is the classic first-timer error: the workflow processes exactly one batch and stops, successfully, with no error.
Genuine caution: because most nodes loop on their own, reaching for Loop Over Items by reflex is the most common piece of unnecessary complexity in n8n workflows. If you don't need batching, ordering, or a once-per-loop "done" moment, you probably don't need the node.
n8n's Code node — where you write JavaScript or Python by hand,
previewed in Chapter 10 (When the Catalog Falls Short) — has a mode
switch that is really a loop switch: Run Once for All Items
hands your code the entire item list (perfect for custom aggregation,
sorting, or deduplication), while Run Once for Each Item
runs your code per item like an ordinary node. Choosing the wrong mode
produces confusing results in both directions, so check it first
whenever a Code node behaves strangely.
Two supporting players deserve a mention: the Limit node truncates the item list to the first N (invaluable while testing loops on a sample), and Remove Duplicates collapses repeated items — a one-node fix for several fan-out accidents described below.
| You want to… | Zapier | Make | n8n |
|---|---|---|---|
| Do a step once per element of a list | Looping by Zapier (explicit) | Automatic once bundles exist; Iterator if the list is inside one bundle | Automatic once items exist; Split Out if the list is inside one item |
| Turn one record's list into separate records | Looping by Zapier, or a line-item-aware action | Iterator | Split Out |
| Collapse elements into one summary within a run | Formatter Utilities > Line-item to Text |
Aggregator (Array/Text/Numeric) with correct Source Module | Aggregate node |
| Collapse events arriving over hours or days | Digest by Zapier | Data store + scheduled scenario (see Chapters 14 and 17) | Stored data + Schedule trigger (see Chapters 14 and 17) |
| Control how many elements are processed at a time | Not meaningfully | Not directly — bound the array upstream or schedule smaller runs | Loop Over Items batch size |
The through-line: on Zapier, plurality is always explicit and slightly effortful; on Make and n8n, plurality is the default and singularity is what you construct, using an aggregator or Aggregate node to put the pieces back together before the final step.
When you do loop explicitly, the batch size — how many elements you process per chunk — is a real design decision, not a default to shrug at. Four forces pull on it:
A sane default: start small — single digits to low tens — prove correctness, then raise the batch size until you approach a rate limit or a runtime you dislike. Moving from small to large is an edit; recovering from a thousand half-processed records is an afternoon.
List-handling bugs almost never announce themselves as list-handling bugs. They present as data corruption, duplication, or spooky nondeterminism. Here is the field guide — symptom, then the list-shaped cause to check first:
| Symptom | Probable cause |
|---|---|
| Values arrive comma-mashed into one field | Zapier flattened line items into a non-line-item field; or a text join happened where a split was needed |
| Only the first element was processed | A mapping picked a single element instead of the list; missing Iterator/Split Out; n8n Loop Over Items missing its return wire |
| A step ran N times when you expected once | Implicit looping: multiple bundles/items arrived. Trace upstream to the node that multiplied them |
| Duplicate records in the destination | Fan-out ran per element but the write step used order-level fields, creating one identical record per element; or Zapier's per-record runs overlapped with a loop |
| One giant aggregate, or aggregates scoped wrong | Make: wrong Source Module. n8n: Aggregate placed
before, not after, the per-item work |
| Downstream steps silently didn't run | Zero-element list: an empty array iterated into nothing, and
everything after starved. In n8n, a node's
Always Output Data setting can keep the branch alive for
explicit empty-handling |
| Works with one test record, breaks in production | The test record had a single-element list; flattening, ordering, and scope bugs all hide at N = 1 |
Two diagnostic habits generalize across platforms. First, always test with plural data — a record whose list has at least two, ideally three, elements, because one-element lists hide nearly every bug in the table. Second, find the count mismatch: every platform's execution view (Zapier's run details, Make's per-module bubbles, n8n's item counts) shows how many times each step ran; the step where the number diverges from your mental model is where the bug lives. Chapter 28 (Diagnosing Failures) builds the general method; Chapter 39 catalogs specific cases.
Loops are the only place in workflow automation where a small mistake multiplies. A bad filter wastes one run; a bad loop can burn a month's allowance in an afternoon. Runaways come in three shapes:
Watch out — cost: The three platforms meter loops very differently, and the difference is dramatic. Zapier bills per task and Make per operation — both count step executions, so a loop multiplies your bill by elements times steps inside the loop. n8n Cloud instead bills per workflow execution, no matter how many items and nodes that run involves — a loop inside one run does not multiply the quota, and sub-workflows the run calls don't add to the count either (only the top-level execution is metered), though every run still consumes time and memory. The actual arithmetic — what a 40,000-row accident costs on each platform, and why the answers differ by orders of magnitude — is the opening act of Chapter 31 (Unit Economics). For now, internalize the shape: on task- and operation-billed platforms, cost scales with elements times steps inside the loop.
The rails, in the order you should install them:
None of these rails costs more than a few minutes to install. The incident they prevent costs real money, a polluted destination system, and a cleanup project — a trade you should take every time.
Splitting, looping, and collapsing are the grammar of every substantial workflow, and each platform speaks it with a different accent: Zapier grudgingly, through line items and a bolted-on loop; Make fluently, through bundles disciplined by the Iterator–Aggregator pair; n8n natively, with lists everywhere and explicit loops reserved for batching. From here, Chapter 14 picks up what happens when collapsed data needs somewhere to live inside the platform, and Chapter 16 turns to the other great flow-control question — not "how many times?" but "which way?"
Every workflow run starts with amnesia. As Chapter 3 (Mental Models: How a Run Actually Happens) explained, a run is born when its trigger fires, lives for seconds or minutes, and disappears when the last step finishes. Nothing it learned survives unless you deliberately put that knowledge somewhere durable. This chapter is about the "somewhere": the small persistence layers each platform builds in — Zapier Tables and Storage by Zapier, Make's data stores, n8n's Data Tables and workflow static data — the handful of patterns that account for nearly all real-world use of them, and honest criteria for when you should graduate to Google Sheets, Airtable, or Postgres instead.
One boundary before we start. Everything here is about data that serves the workflow itself: deduplication registries, lookup tables, queues, counters, state handed from one run to the next. Feeding stored content to an AI step as knowledge — so a chatbot can answer questions from your documents, say — is a different discipline with different tools, and it lives in Chapter 24 (Knowledge, Memory, and Chat Surfaces).
Five recurring situations force a workflow to remember something between runs:
region: EMEA-3 and you need to know that means "route to
Priya's team." Somebody has to store that mapping, and it changes too
often to hard-code into the workflow.Every platform's built-in store exists to answer these five questions cheaply, without making you sign up for a database. The stores differ in shape, though, and the shape determines which patterns feel natural on each.
Two shapes cover everything you will meet. A key-value store is the simplest possible memory: you save a value under a name (the key), and later retrieve it by that exact name. Fast, tiny, no structure. A table is the familiar spreadsheet shape — rows (records) with named columns (fields) — which adds the ability to search by any column, not just look up by one key. Key-value suits counters and bookmarks; tables suit anything with more than one attribute per item.
Zapier gives you two options at very different levels of ambition.
Storage by Zapier is the minimalist one: a key-value store you use as ordinary steps inside a Zap. Search the app picker for "Storage by Zapier" and you will find actions like Set Value, Get Value, Increment Value, and Remove Value, plus list operations that push a value onto a list or pop one off. There is no browsing interface anywhere in the Zapier dashboard — the only way to see what is stored is to read it back from a Zap or a code step. Access is controlled by a secret: an arbitrary password-like string you invent when you first use it. Every Zap (and every person) using the same secret sees the same data, which is both the sharing mechanism and the entire security model.
Watch out: The Storage by Zapier secret is the whole key to the data — there is no account-level recovery, no permissions screen, and no way to enumerate your secrets later. Treat it like a password: generate something long and random, record it in your password manager, and never reuse a guessable phrase. Lose it and the data is effectively stranded; leak it and anyone can read and overwrite your values.
Two details make Storage more capable than it looks. First,
Increment Value is atomic — meaning the platform performs the
read-add-write as one indivisible operation, so two Zap runs
incrementing at the same moment cannot overwrite each other. That makes
it the safest counter primitive on any of the three platforms. Second,
if you write JavaScript or Python in a Code by Zapier step, a small
client library (StoreClient) gives you the same storage
programmatically, which is handy for reading several keys at once. The
constraints are real, though: each value is capped at a small size
(kilobytes, not megabytes), each key name must be short (a few dozen
characters — hash long identifiers before using them as keys), each
secret holds a limited number of keys (hundreds, not thousands), and
there is no way to search — you must know the exact key. One quirk to
file away: an untouched key expires on its own after a few months of
inactivity, which conveniently bounds a dedup registry but can also
quietly erase a bookmark a workflow stopped reading.
Zapier Tables is the ambitious one: a genuine table
product with typed fields, a spreadsheet-like editing screen, and
first-class Zap integration. You reach it from Tables in
the left navigation of the Zapier home screen. Tables can
trigger Zaps (new record, updated record), be written by Zaps
(Create Record, Update Record), and be queried
(Find Record). Because Tables have a real interface, humans can
review and edit the data — a support manager can open the routing table
and change who owns the "enterprise" segment without anyone touching the
Zap. Tables also connect to Zapier's Interfaces product for forms and
simple apps, and support button fields that fire Zaps from a row, which
starts to overlap with the human-in-the-loop patterns of Chapter 20
(Humans in the Loop). Record capacity scales with plan tier: lower tiers
hold a few thousand records, higher and dedicated Tables tiers
substantially more. Check the current limits page before designing
anything around a specific number.
Worth a passing mention: Digest by Zapier, a built-in that accumulates entries and releases them as one batch on a schedule or threshold. It is really an aggregation tool — Chapter 13 (Lists, Loops, and Aggregation) covers that territory — but it quietly solves the "collect now, act later" cases you might otherwise build a queue for.
Make has one persistence product, and it sits closer to a small
database than either Zapier option. A data store is a
named container of records that lives at the team level — you will find
Data stores in the left-hand navigation, near where
scenarios live. Each data store is (optionally but usually) bound to a
data structure: a reusable schema, which is simply a
formal list of the fields a record contains and their types (text,
number, date, boolean, collection, and so on). Data structures are
defined once, under their own item in the same navigation area, and
reused anywhere Make needs to understand a record's shape.
Inside a scenario, a family of data store modules does the work: Add/replace a record, Update a record, Get a record, Search records, Delete a record, and Count records. The detail that matters most: every record has a key, and you choose it. If you set the key to something naturally unique — an order number, an email address — the data store becomes a deduplication machine with no extra logic, because Add a record with "overwrite" disabled will error on a duplicate key (which you can catch with an error handler, per Chapter 19, Error Handling by Design), while Get a record by key tells you instantly whether you have seen the item before.
Tip: In Make, always choose a natural key — the ID the outside world already uses for the item — rather than letting the platform generate one. A generated key gives you a record you can only find again by searching; a natural key gives you free deduplication and one-step lookups.
Data stores have a browsable interface (open the data store and inspect or hand-edit records), which makes them usable as small lookup tables that a teammate maintains. Capacity is a pooled allowance: your organization gets a total data store budget measured in megabytes that grows with plan tier, shared across all stores, plus a cap on how many stores you can create. It is modest — think thousands to tens of thousands of lean records, not millions — and when you hit it, writes start failing, so treat cleanup as part of the design rather than an afterthought.
Make also has Set variable and Get variable modules, and it is important not to confuse them with storage: their scope options ("one cycle" or "one execution") mean the value evaporates when the run ends. They pass data within a run, not between runs. Organization- and team-level custom variables (a paid-tier feature) do persist between runs, and a scenario can even update them through a dedicated module — but each holds a single value and they are meant for shared configuration or business logic that changes occasionally (an environment name, a current campaign code), not the per-record operational state a data store is built for.
n8n's native answer is Data Tables, a relatively recent addition, so if you are reading older tutorials you will see community workarounds (Google Sheets, Redis, "use the workflow's own database") that are no longer necessary for small cases. A data table is a typed grid — columns of text, number, boolean, or date — that lives inside a project, alongside your workflows and credentials, with a spreadsheet-style screen for creating columns and hand-editing rows. In a workflow, the Data Table node provides the verbs: insert rows, get (filter) rows, update rows, delete rows, and — the one to learn first — upsert, a portmanteau of update-or-insert that updates a matching row if one exists and creates it otherwise. Upsert is the key to idempotent workflows: ones that can safely run twice on the same input without doubling anything up. Capacity is an instance-wide storage budget — plan-based on n8n Cloud, configurable by environment variable if you self-host — sized for operational state, not analytics archives.
The second, older mechanism is workflow static data:
a JSON object attached to each workflow that survives between
executions. In a Code node you call
$getWorkflowStaticData('global'), read or mutate the object
it returns, and n8n saves it automatically when the execution ends. It
is the natural home for a single bookmark timestamp or a tiny
per-workflow setting — anything bigger belongs in a data table, because
static data is stored inside the workflow's own database record and
bloats it.
Watch out: Workflow static data is only persisted for production executions — runs started by a real trigger on an active workflow. When you press the test/execute button in the editor, the workflow runs but the static data changes are thrown away. Builders lose hours to this: the bookmark logic works perfectly in tests, appears "broken" in tests forever after, and only behaves once the workflow is activated. Test watermark logic by activating the workflow and firing the trigger for real.
Two more n8n notes. The Remove Duplicates node has a mode that remembers values across executions — "remove items processed in previous executions" — giving you cross-run deduplication with zero code and zero table management (it keeps its own history, which you can clear from the node when you need to reset). And because self-hosted n8n already sits on a database you control, the distance between "built-in store" and "real database" is shorter here than on the other platforms; more on that in the graduation section.
| Store | Shape | Human-editable UI | Searchable | Typical capacity | Best first use |
|---|---|---|---|---|---|
| Storage by Zapier | Key-value | No | No (exact key only) | Hundreds of small values per secret | Counters, bookmarks, tiny flags |
| Zapier Tables | Table | Yes | Yes (Find Record) | Thousands to much more by tier | Dedup registry, lookup table, review queue |
| Make data store | Table with chosen keys | Yes | Yes (Search records) | Megabyte allowance pooled per org | Dedup by natural key, lookups, small queues |
| n8n Data Tables | Table | Yes | Yes (filtered get, upsert) | Instance/plan storage budget | Everything operational, via upsert |
| n8n static data | Key-value (per workflow) | No | No | Tiny (kilobytes) | One workflow's watermark or settings |
Almost every legitimate use of a built-in store is one of the five patterns below. Learn them once; the platform differences are just spelling.
The pattern: derive a stable unique key from each incoming item (order ID, message ID, email address plus date — whatever the source guarantees unique), check the store for it, skip the item if present, and record the key if not. This protects you against webhook retries, overlapping polling windows, users double-clicking submit, and your own re-runs while debugging.
On Zapier, use a Tables Find Record step on the key, then a Filter step (Chapter 16, Branching) that only continues when no record was found, then Create Record to register the key. (Storage by Zapier can do it too — Get Value, filter on empty, Set Value — but Tables gives you a visible registry you can audit.) On Make, the natural-key trick above collapses the whole pattern into one module: Add a record with overwrite off, and an error route that treats "duplicate key" as "already processed, stop quietly." On n8n, either the Remove Duplicates node in cross-execution mode (no code, no table) or a Data Table get followed by an IF node, with an insert on the new-item branch — or simply structure the whole workflow around upsert so duplicates overwrite themselves harmlessly instead of multiplying.
Note the built-in help you already get: polling triggers on all three platforms deduplicate what they deliver (Chapter 8, Triggers: How Workflows Start, explains how). The store-based pattern is for everything the trigger layer cannot see — retried webhooks, multiple entry points feeding one process, or "run this backfill again without re-emailing everyone."
The pattern: a small reference table maps a code the workflow receives to the richer information it needs — SKU to product name and price band, country to sales region, customer tier to SLA hours (SLA — service-level agreement, the response time you have promised), error code to human explanation. The workflow reads with one lookup step; a human maintains the table through the store's editing screen without ever opening the workflow.
This is where the human-editable stores shine. A Zapier Table or Make data store holding thirty routing rows is genuinely better than the same data hard-coded into branch conditions, for one reason: change management. When the routing changes, an operations person edits a row; nobody edits, retests, and republishes the workflow itself. On n8n, a data table plays the same role, and the grid screen means "update the discount matrix" is a task you can hand to anyone with project access (see Chapter 29, Teams, Governance, and Change Management, for who should have that access).
The judgment call is size and origin. A lookup table that a human owns and that changes weekly belongs in the platform. A lookup table that is really a copy of another system's data — your product catalog, your customer list — should be fetched from that system, or synced to a proper external table, because a hand-maintained copy will drift from the truth and nothing will tell you.
The pattern: one workflow enqueues — appends incoming items to the store — and a second, scheduled workflow drains: it wakes up, reads a batch, processes each item, and deletes what it finished. This decouples arrival rate from processing rate, which is the polite way to feed a rate-limited API (Chapter 17, Waiting, Scheduling Windows, and Throttling, covers the timing side) and the only way to batch many events into one digest or one bulk API call.
On Make: Add a record to enqueue; a scheduled scenario runs Search records (oldest first, limited batch), processes, and Delete a record per success. On n8n: the same with Data Table insert, get, and delete, on a Schedule Trigger. On Zapier: Tables can back a queue the same way (a scheduled Zap that searches the table for the oldest pending rows), Storage's push/pop list operations handle small ones, and Digest by Zapier covers the common special case where "processing the batch" just means "send it all at once."
Two disciplines keep queues honest. Delete only after successful processing, never before — otherwise a mid-batch failure silently loses items. And record a status or attempt count on each item rather than assuming one pass always succeeds, so a poison item (one that always fails) cannot jam the queue forever; after a few attempts, move it to a "needs a human" state (Chapter 20 again).
The pattern: increment a number per event, read it to enforce a rule — "no more than 50 outreach emails per day," "alert on the fifth failure," "every 100th signup gets a gift." The subtle danger is the race condition: if two runs execute read-then-write at the same moment, both read 41, both write 42, and one increment is lost. The safe version requires an atomic operation — one the platform guarantees happens indivisibly.
Zapier's Increment Value in Storage is atomic, which makes
Zapier the most trustworthy platform for counters that matter. Make and
n8n offer no atomic increment on their stores; a get-add-update sequence
is vulnerable whenever runs can overlap. The mitigation on both is to
remove the overlap: run the counting workflow sequentially (Make
scenarios can be set to sequential processing in scenario settings; n8n
concurrency can be capped so runs queue instead of overlapping, and
Chapter 30, Scale, Volume, and Performance, discusses concurrency
controls at length). For counters that reset — daily caps — embed the
window in the key itself (sent-2026-07-16) instead of
resetting a single key, and old windows simply go stale.
Tip: If a counter guards something genuinely costly — money, compliance, a hard API quota — do not trust a non-atomic store with it under concurrency. Either serialize the workflow so runs cannot overlap, use Zapier's atomic increment, or graduate that one piece of state to Postgres, where
UPDATE ... SET n = n + 1is atomic by nature.
The pattern: a scheduled workflow stores a watermark — a bookmark, usually the timestamp or highest ID of the last item processed — and each run asks the source for "everything after the watermark," processes it, and advances the watermark. This is how you build reliable "sync new rows," "import new orders," and "catch up after downtime" workflows when the source has no usable trigger.
This is exactly what n8n's workflow static data is for: read
lastSeen, fetch since then, write the new high-water mark —
three lines in a Code node, or the same shape with a one-row data table
if you prefer no code. On Zapier, a single Storage key does it; on Make,
a one-record data store.
Three rules make watermarks robust. Advance the watermark only after processing succeeds, so a failed run retries the same window instead of skipping it. Overlap the window slightly (ask for "since watermark minus a few minutes") and rely on your dedup pattern to absorb the repeats — clocks and API timestamps are less precise than they look. And use the source's timestamps, never "now" on your side, or items created during the run's own execution can fall between two windows and vanish.
A few realities apply across all three platforms, and they are where store-backed designs quietly fail.
Capacity is small and finite, and most of it never expires on its own. With one exception — Storage by Zapier drops keys after a stretch of inactivity — these stores keep whatever you write until you delete it, and all of them have caps: key counts, record counts, or a byte allowance. A dedup registry or queue only ever grows — never shrinking on its own — until, months later, writes start failing in a workflow nobody has touched. Build the janitor on day one: a scheduled workflow that deletes dedup keys older than your longest realistic retry horizon and clears completed queue items. This is not optional hygiene; it is part of the design.
Watch out: Store-full failures are the classic delayed-fuse bug: everything works in testing, works for weeks in production, then fails on a Tuesday for reasons invisible in the workflow itself. When a long-stable workflow starts erroring on write steps, check store capacity before anything else — and see Chapter 28 (Diagnosing Failures) for the general triage method.
Know what a store operation costs — the platforms differ sharply. On Make, every data store module call consumes operations like any other module, so a dedup check adds a per-item cost to every run, and a chatty queue-drain loop can be surprisingly expensive at volume. Zapier currently goes the other way: steps from its built-in tools — Storage, Tables, Digest, and their kin — do not count against the task allowance, which makes store-heavy Zap designs cheaper than they look (pricing rules do change; confirm on the current pricing page before betting a design on it). On n8n, pricing follows executions rather than steps, so store operations inside a run are effectively free. These are structural differences that matter at scale, and one of the economics threads picked up in Chapter 31 (Unit Economics). Stores are also rate-limited like any app: a loop hammering the store hundreds of times in seconds can be throttled. Batch reads where the platform allows it.
Concurrency is the silent corrupter. Beyond counters, any read-modify-write on shared state — appending to a list value, updating a status field two runs both loaded — can lose data when runs overlap. The three defenses, in order of preference: design idempotently so overlapping writes converge on the same result (upserts with natural keys), serialize the workflow so overlap cannot happen, or move the contested state to a real database with transactions.
Stores are invisible to your other tools. Data in a platform store cannot be joined, charted, backed up on your schedule, or read by anything outside the platform without building an export workflow. It also deepens lock-in: workflows can be rebuilt on another platform, but data has to be migrated, and platform stores rarely export cleanly (Chapter 34, Migration, Coexistence, and Lock-In, treats this properly). The rule of thumb that keeps you safe:
Tip: Treat the built-in store as a notebook, not a filing cabinet — operational scratch state the workflow could regenerate or survive losing. The moment data becomes a record (customers, orders, anything with an owner, an audit need, or a second consumer), it belongs in a system whose first job is keeping data.
The built-in stores are the right default for the five patterns at modest scale. Graduate when any of these become true:
Where to graduate to depends on which pressure you feel:
| Destination | What you gain | What you accept | Reach for it when |
|---|---|---|---|
| Google Sheets | Universal human access, zero cost, instant sharing | API rate limits; lookups scan rather than query; type quirks (dates, leading zeros); fragile under concurrent writers; hard cell ceilings | Humans are the main consumer and volume stays modest |
| Airtable | Typed fields, views, forms, linked records, a decent query API | Per-base record caps by plan; modest API rate limits; per-seat cost; still not a transactional database | The data needs structure and a friendly interface for a team |
| Postgres (self-run or managed, e.g. Supabase/Neon) | Real indexes and joins, transactions, atomic updates, effectively unbounded scale, standard SQL any tool can read | You own credentials, schema changes, backups, and network security; every platform call to it still costs a step | Volume, concurrency, or reporting demands are real — or the data is a system of record |
All three automation platforms connect to all three destinations through ordinary app integrations (Sheets and Airtable natively; Postgres via each platform's database modules — with the credential and network-security care described in Chapter 7, Connections and Credentials). n8n deserves a special note: if you self-host it, a Postgres database is already in your architecture, so "graduating" can mean adding one table on infrastructure you run anyway — which is why heavy n8n shops skip the middle rungs and go from Data Tables straight to SQL. On Zapier and Make, Sheets or Airtable is usually the comfortable first step off the platform, and Postgres the destination you choose deliberately when the data has become an asset.
The tradeoff to keep in view is friction versus ownership. Every rung down that table adds setup, credentials, and things you must maintain — and removes a platform limit, a lock-in tie, and a class of silent failure. Neither endpoint is virtuous in itself. A dedup registry in a Make data store is a perfectly finished design; so is an order ledger in Postgres. The mistake is the mismatch: customer records trapped in a store nobody can query, or a three-key watermark solemnly maintained in a dedicated database.
A compact decision path, in the spirit of Chapter 4 (The Rosetta Stone, the Capability Map, and How to Decide):
Do that, and the persistence layer becomes what it should be: a few small, boring tables that make your workflows honest — never running twice on the same input, never losing their place, and never surprising you at 2 a.m. with a store that filled up in silence.
Everything in this volume so far has treated data as text you can read: names, amounts, dates, IDs, sitting in fields you can map from one step to another. Files break that assumption. A PDF invoice, a product photo, a spreadsheet export — these are binary data: long runs of raw bytes that only make sense to software that understands the file's format. You cannot glance at the bytes of a JPEG and see a photo, and neither can your automation platform. It can only carry the bytes faithfully from one place to another, hand them to an app that understands them, or run them through a converter that turns them into something structured.
That "carry faithfully" job sounds trivial and is not. Each of the three platforms represents a file differently, moves it through a run differently, and enforces different size and storage limits — and those differences are invisible until the day a 40 MB attachment silently fails or a CSV with a stray comma corrupts a hundred rows. This chapter covers the deterministic mechanics: moving files between apps, handling email attachments, parsing CSVs into records, extracting text from PDFs, and converting formats. Using AI to understand documents — reading a messy invoice with a language model rather than a template — is a different discipline with different failure modes, covered in Chapter 21 (AI Steps Inside Ordinary Workflows).
Strip away the icon and the preview, and a file is three things:
invoice-4471.pdf. Purely
a label; nothing stops a PDF from being named
photo.jpg.application/pdf or image/png that tells
receiving software what format the bytes claim to be. ("MIME" is a
legacy acronym from email standards; today it just means "content type
label.")Platforms must also decide how to move the bytes through a run. There are two honest strategies:
One more concept you will meet constantly: Base64 encoding. Workflow platforms speak JSON internally (see Chapter 11, Three Data Models), and JSON can only hold text. Base64 is a scheme that rewrites arbitrary bytes as a long string of ordinary characters so binary content can ride inside a text field. It works, but the encoded version is roughly a third larger than the original file — which matters when you are near a size limit.
Tip: Trust the MIME type over the filename extension when filtering files, but trust neither absolutely. Email clients and legacy systems mislabel files routinely. If a step downstream will break on the wrong format, let it fail loudly and route the failure (see Chapter 19, Error Handling by Design) rather than assuming the label was honest.
This is the core mental model of the chapter. Get this straight and the rest is details.
Zapier passes files by reference. When a trigger or action produces a file, downstream steps see a file object — an opaque pointer, not the bytes. In the field mapper and in test data, file content shows up as a placeholder along the lines of "(exists but not shown)" rather than a preview. When a later step actually needs the content — say, an upload action — Zapier fetches the real bytes at that moment. Zapier calls this deferred fetch hydration: the reference is a dry token, and it gets "hydrated" into real content only on demand.
Two practical consequences:
Watch out: Never write a Zapier file reference or a temporary download link into a spreadsheet or database as your "copy" of the file. The link expires, and weeks later you will have a column of dead URLs. If you need the file kept, upload it to real storage (Drive, Dropbox, S3) inside the Zap and record that app's permanent link or file ID instead.
Make passes files by value. In Make's data model,
every module outputs bundles (its term for the packets of data flowing
between modules — see Chapter 11), and a file-producing module includes
the actual binary content as a data field inside the
bundle, alongside a separate fileName field. When you map a
file into an upload module, you map those two fields explicitly — the
bytes and the name are independent, which is flexible (rename on the fly
by mapping different text into the name field) and a classic source of
mistakes (mapping the name and forgetting the data).
Because the bytes genuinely travel through the scenario, Make gives
you functions to cross between the text world and the binary world:
toString() turns binary data into text (essential before
parsing a CSV's content), and toBinary() turns text into
file content (essential when you have generated, say, CSV text and want
to upload it as a file). Make enforces a maximum size for any single
file a scenario handles, scaled by plan tier — larger allowances on
higher-tier plans — and a scenario that hits an over-limit file errors
rather than truncating.
n8n splits every item flowing through a workflow into two
compartments: json (the structured fields you have worked
with all volume) and binary (zero or more named file
attachments riding along with the item). In the editor, a node's output
panel shows Table and JSON views and,
when files are present, a Binary tab — which lists each
file with its name, MIME type, size, and a download button so you can
inspect exactly what a node produced.
The word "named" matters. An item can carry several files at once,
each under a binary property name: a Gmail trigger that
downloads attachments will typically expose them as
attachment_0, attachment_1, and so on, while
an HTTP download usually lands in a property called data.
Downstream nodes that consume files ask you which property to read — an
"Input Binary Field" setting or similar — and the single most common n8n
file bug is a node pointed at data while the actual file
sits in attachment_0. The Binary tab tells you the truth;
check it.
Where the bytes physically live depends on how n8n is run. By default
they are held in memory during execution. Self-hosted operators can
switch binary data to filesystem storage (an environment setting,
N8N_DEFAULT_BINARY_DATA_MODE, in current versions), which
trades a little speed for the ability to handle much larger files
without exhausting RAM; hosted n8n manages this for you within plan
limits. Hosting trade-offs generally are Chapter 32's territory.
| Question | Zapier | Make | n8n |
|---|---|---|---|
| How is a file represented? | File object (reference), hydrated on demand | Binary data field inside the bundle, plus separate
fileName |
Named entries in an item's binary compartment |
| Do the bytes travel through the run? | Only when a step consumes them | Yes, by value | Yes (memory by default; filesystem configurable when self-hosting) |
| How do you inspect a file mid-run? | Placeholder text in test data; verify via the destination app | Bundle inspector shows file name and size | Binary tab with metadata and download |
| Map a URL into a file field? | Yes — Zapier downloads it | Use an HTTP "Get a file" module first | Use an HTTP Request node set to return a file first |
| Multiple files on one record? | Usually surfaced one per run or as line items | Arrays of files; you iterate over them | Multiple named binary properties on one item |
The bread-and-butter file job is a relay: something produces or stores a file in app A, and you need it in app B. The pattern is always acquire → (optionally transform) → upload, and the platform differences are all in the acquire and the mapping.
If the source app has a native trigger or action that outputs the file itself (Gmail attachments, a Drive "new file" trigger, a form tool's upload field), use it — the file arrives already in the platform's native representation. If the source app only gives you a download URL, fall back to HTTP: map the URL straight into the destination's file field on Zapier, or insert an explicit download step on Make (an HTTP module fetching the file) or n8n (an HTTP Request node with its response configured to be treated as a file, which lands the content in a binary property). Raw HTTP mechanics, including authenticated downloads, are Chapter 9's subject.
One ecosystem quirk ambushes nearly everyone eventually: files native to an office suite — a Google Doc or Google Sheet, say — are not ordinary binary files at all, and cannot be downloaded as-is. The download action makes you choose an export format (PDF, Word, Excel), and what travels onward is that exported copy, not "the file." If a relay that works fine on uploaded PDFs mysteriously fails on certain items in the same folder, check whether those items are suite-native documents.
Upload actions across all three platforms want the same ingredients: the file content, a filename, and usually a destination folder. The trap is mapping the wrong thing into the content slot.
Watch out: On Make and n8n, mapping a URL string into a field that expects file bytes does not fetch the URL. Depending on the app, you either get an error or — worse — a successfully uploaded "file" that is actually a few dozen bytes of text containing the URL itself, with a
If both apps live in the same ecosystem, you often do not need to move bytes at all. Copying a file from one Google Drive folder to another, or attaching an existing Drive file to a record elsewhere, can frequently be done by passing the file's ID to a native "copy" or "share" action. Passing references is faster, immune to size limits, and — on platforms that meter usage — cheaper. Download-and-reupload is for crossing ecosystem boundaries, not for shuffling within one.
Tip: Before building a download → upload relay, check whether the destination app's action list includes something like "Copy file", "Attach by ID", or "Import from URL". Letting the apps move the file between themselves, with the workflow only passing identifiers, sidesteps every limit in this chapter.
Email is still how the business world moves documents, so attachment handling is worth its own mechanics section. The trigger side of email is covered in Chapter 8 (Triggers: How Workflows Start); here is what happens to the files.
Zapier flattens the problem for you. Gmail's integration offers a trigger specifically for new attachments, and it fires once per attachment — an email with three PDFs produces three separate runs, each carrying one file object plus the email's metadata (sender, subject, date). That means no looping logic: each run handles exactly one file. The cost is that "process all attachments of one email together" becomes awkward, because each run is independent.
Make hands you the email as one bundle in which
attachments are an array — a list of zero or more files. To act
on each attachment you insert an Iterator (Make's
module for turning an array into a stream of individual bundles; the
general pattern is Chapter 13's subject) pointed at the attachments
array, then filter the stream — for instance, keep only bundles whose
filename ends in .pdf. More setup than Zapier, but the
whole email and all its files stay together in one scenario run, which
makes "combine, then act" logic natural.
n8n's email triggers (Gmail, Outlook, or generic
IMAP — IMAP being the standard protocol for reading mailboxes) have an
option to download attachments; enabled, each email arrives as one item
with the files stacked in its binary compartment as numbered properties
(attachment_0, attachment_1, …). If you need
to treat each attachment as its own item, a splitting step converts the
numbered properties into one item per file; if you only ever care about
the first attachment, you can point downstream nodes straight at
attachment_0.
In all three, filter early. Signatures, inline logos, and calendar
invitations all arrive as attachments too, and a workflow that files
every image001.png into your invoices folder is a workflow
you will be cleaning up after. Filter on MIME type or extension
and, where layouts allow, on sender or subject.
A CSV ("comma-separated values") file is the lingua franca of exports: plain text, one record per line, fields separated by commas, usually with a header line naming the columns. It is also the format most likely to look trivial and bite you, because the platform sees a CSV as one opaque file until you explicitly parse it — turn it from a blob into structured records you can map.
Each platform has a native parser:
Formatter → Utilities → Import CSV File. Feed it the file
object; it returns the rows as line items — Zapier's
representation of a list inside a single run, which Chapter 13 unpacks.
Downstream steps that understand line items (like "create multiple
spreadsheet rows") can then consume all rows; steps that don't will need
the looping techniques from that chapter.toString(). You tell
the parser whether the first row is headers and what the separator is,
and it emits one bundle per row, ready for filtering and mapping like
any other bundles.The format's traps are universal and worth naming because every one of them produces silently wrong data rather than an error:
"Smith, Jones & Co" is legal CSV — the quotes protect
the comma. Competent parsers handle this; hand-rolled splitting in a
code step does not. Use the parser.id or names with mangled
accents. Prefer UTF-8 exports at the source when you control it.Tip: Before trusting any CSV workflow, feed it a deliberately nasty test file: a field with an embedded comma, a field with a quoted line break, an accented name, and an empty trailing column. Five minutes of sabotage now beats discovering in month three that every row containing a company called "Anderson, Lee & Partners" has been shifted one column right.
PDFs come in two fundamentally different kinds, and knowing which you have decides everything:
For text-layer PDFs, the platform picture is unusually lopsided. n8n extracts natively: the same Extract from File node has a PDF operation that reads a binary property and returns the document's text (and basic metadata like page count) as JSON fields — no third-party service, no extra cost. Zapier and Make have no comparably general built-in PDF-to-text step; the standard route on both is a connected document service — either a conversion service that turns the PDF into text (PDF.co and CloudConvert are common) or a template-based document parser (Docparser and Parseur are well-known examples). Template parsers deserve a moment, because they are the deterministic answer to structured documents: you upload a sample of a known layout and draw rules — "invoice number is the text following the label 'Invoice #'", "the total is in this region of the page" — and the service applies those fixed rules to every future document of that layout. No model, no guessing: the same input produces the same output every time, which is exactly the property this chapter's methods promise. The trade-off is rigidity — a vendor redesigns their invoice and your template extracts nothing until you update it.
Once you have raw text, the last step is pulling out the fields you
actually want, and all three platforms can do that deterministically
with pattern matching: Zapier's Formatter can extract text by pattern,
Make has a text-parser module built for regular-expression matching, and
n8n handles it in a Code node or expression. A regular expression (a
compact pattern language for finding text —
Invoice\s*#?\s*(\d+) finds "Invoice # 4471" and captures
the digits) is the workhorse here. Chapter 12 (Mapping and Transforming
Fields) covers the transformation toolbox in depth.
Format conversion — spreadsheet to CSV, image resizing, HTML to PDF, anything to anything — splits into two tiers.
Native, structural conversions the platforms handle themselves. n8n's Convert to File / Extract from File pair moves data between JSON items and CSV, spreadsheet, text, or other file shapes; Make has modules for creating and unpacking archives (ZIP files) and for building CSV text from bundles; Zapier's Formatter covers text-level reshaping. If the conversion is really "restructure data and change its container," look here first — it is free of third-party dependencies.
True media and document conversions — Word to PDF,
image format changes and resizing, HTML rendered into a polished PDF —
are delegated to connected conversion services on all three platforms
(CloudConvert is the ubiquitous example, with many alternatives in each
catalog; see Chapter 6, The App Catalogs). The workflow pattern is
always the same relay: send the file and target format, receive the
converted file, carry on. One direction deserves special mention because
it turns automation from a consumer of documents into a producer:
generating documents by filling a template (a Google Docs or
Word template with {{placeholders}} mapped from your data)
and converting the result to PDF. That pattern — data in, branded PDF
out — powers automated quotes, certificates, and reports, and every
piece of it is deterministic.
File workflows fail differently from data workflows: they work perfectly in testing with a 200 KB sample and die months later on the first real 60 MB file. Build with the limits in view.
| Limit | Where it bites | What to do |
|---|---|---|
| Per-file size ceilings | All three platforms cap the file size a run may carry; the ceiling varies by platform and plan tier, and is generous for documents but easy to breach with video, audio, or bulk exports | Check current plan documentation before designing; for large media, pass references or direct links between apps instead of bytes |
| Temporary file references expiring | Zapier file objects and most platform-generated download links are short-lived | Persist files to real storage in-run; store the storage app's permanent ID or link |
| Memory pressure | Make scenarios and n8n executions carry bytes by value; several large files in one run multiply the footprint. Self-hosted n8n is bounded by its server's RAM | On self-hosted n8n, switch binary data to filesystem mode; everywhere, split huge batches into smaller runs (Chapter 30 covers volume strategies) |
| Metered usage | File steps are metered like any other: every download, upload, or conversion step consumes tasks/operations, and some plans also meter total data transferred on top of that | Skip unnecessary relays; filter before downloading, not after (Chapter 31, Unit Economics, does the arithmetic) |
| Execution-history retention | Files attached to past runs are pruned with run history — n8n's stored binary data, for example, lives and dies with execution logs | Never treat run history as an archive; Chapter 14 (Storing Data Inside the Platform) draws the storage line precisely |
| Step and run timeouts | A slow origin server serving a big download can push a step past its time budget | Prefer origins with fast direct-download links; for chronically slow sources, decouple with a webhook hand-off (Chapter 9) |
Watch out: The most dangerous property of size limits is when they fail. Everything in your tests was small, so the workflow shipped green — then someone emails a scanned 80-page contract and that one run dies while the surrounding runs succeed. Decide up front what should happen to over-limit files (skip and notify is usually right), and build that branch deliberately using the patterns from Chapter 19. An unhandled size failure is an invoice that silently never got filed.
The scenario: supplier invoices arrive as PDF attachments at an
accounts-payable (AP) email address. For each invoice you want to (1)
pull out the invoice number, vendor name, amount, and date; (2) file the
PDF into a cloud-storage folder with a clean name like
2026-07-ACME-4471.pdf; (3) append a row to a spreadsheet
that links to the filed copy. Your suppliers use consistent invoice
layouts, so deterministic extraction — a template parser or pattern
matching on extracted text — is the right tool; no AI required.
.pdf — this stops signature images cold.{{date}}-{{vendor}}-{{invoice_number}}.pdf.Each invoice consumes a handful of tasks. Simple, robust, and the parser subscription is the moving part to watch.
.pdf.Invoice\s*#?\s*(\d+) and friends). toString()
is your bridge anywhere a module hands you binary where text is
needed.data into the file-content field and your
composed name into the filename field. Both, explicitly — this is the
platform where forgetting one is possible.One scenario run handles a whole email, however many invoices it carried, and you can see every bundle in the run inspector when something looks off.
attachment_0, attachment_1, …application/pdf.invoice_number, vendor,
amount, date fields.The distinctive feature: zero third-party parsing dependencies for text-layer PDFs. The trade-off: the regexes live in your workflow as code you maintain, rather than in a parser service's template UI — and if your instance is self-hosted, big attachment days are your server's memory problem.
The destination is identical; the shape of the work is not. Zapier removed the loop (one run per attachment) and outsourced the parsing; Make made the loop explicit and kept everything visible in one run; n8n kept the entire pipeline in-house at the cost of owning the extraction logic. That is this compendium's recurring pattern in miniature — the platforms agree about what and disagree about where the complexity lives — and it is exactly the kind of trade-off Chapter 35 (The Decision Framework) teaches you to weigh deliberately. When your invoices stop being consistent and templates start failing, that is your cue to read Chapter 21 — the AI chapter — with this deterministic baseline as your point of comparison.