Choosing an Automation Platform — Volume C: Data

Choosing an Automation Platform — Volume C: Data

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:


Three Data Models

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, Taught Gently

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:

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 phone key 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: Flat Fields

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.

Line items: Zapier's answer to arrays

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:

The 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.

Where to inspect data in Zapier

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 Out in the actual Zap run rather than the editor's remembered examples.

Make: Bundles and Data Structures

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.

Data structures: Make's explicit schemas

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.

Where to inspect data in Make

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: Items as JSON

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.

Where to inspect data in n8n

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.

The Same Order, Three Ways

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

Answering "What Did This Step Really Receive?"

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:

  1. Zapier: open the run history, click the specific run, select the step, read 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.
  2. Make: open the scenario run (live, or via the History tab), click the module's bubble, and read the input bundles — per operation, if it ran more than once.
  3. n8n: open the execution from the 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.

Why the Data Model Shapes Difficulty

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.

Where This Goes Next

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.


Mapping and Transforming Fields

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).

How Mapping Works: One Idea, Three Pickers

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.

Zapier: the dropdown of samples

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.

Make: the mapping panel and its pills

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 commasreplace(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.

n8n: drag from the input panel

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.

What breaks a mapping

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.

The Three Transformation Toolkits

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 Formatter: transformation as extra steps

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: functions typed into the field itself

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: expressions everywhere, plus the Edit Fields node

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 variable module, 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.

The Standard Problems, Solved Three Ways

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.

Cleaning text: trim, split, join

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.

Name casing

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.

Numbers and currency

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.

Lookup tables: translating codes into words

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.

The Date and Timezone Trap

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.

What a date string really is

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.

Why 2026-07-17 can mean two different days

Here 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.

The two-step habit: parse, then format

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.

Format tokens are not portable

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-DD pasted into an n8n Luxon .toFormat() does not produce a calendar date — in Luxon, capital DD means 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.

Where the default timezone hides

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).

Habits That Keep Mappings Healthy

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.


Lists, Loops, and Aggregation

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.

The Three Moves

Every many-at-once problem decomposes into at most three moves:

  1. Split — one record that contains a list becomes many records, one per element. An order with five products becomes five product records.
  2. Process each — the loop itself: do something once per element. Look up each product's stock level; send each attendee their own email.
  3. Collapse — many records become one. Forty ticket records become one digest email; five product records become one order-summary row. This is aggregation.

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: One Record Per Run, Line Items on the Side

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.

Line items and their notorious awkwardness

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):

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.

The Formatter utilities: Zapier's line-item toolkit

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

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:

Collapsing across runs: Digest by Zapier

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: The Iterator and the Aggregator

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: one bundle into many

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 Aggregators: many bundles into one

The Aggregator family does the reverse — consumes a stream of bundles and emits a single bundle:

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 Module before anything else. The aggregator's scope is defined by that setting, not by its position on the canvas.

The iterate-then-aggregate pattern

The canonical Make shape sandwiches per-element work between the two:

  1. Order arrives (one bundle, products in an array).
  2. Iterator explodes the array — five bundles.
  3. A lookup module runs per bundle — five stock checks.
  4. Array Aggregator with Source Module set to the Iterator — one bundle again, now enriched.
  5. One email, one invoice row, one Slack message.

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: Everything Is Already a List

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.

Split Out and Aggregate

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.

Loop Over Items: when you must be explicit

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.

The Code node's two modes

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.

Implicit or Explicit? The Decision in One Table

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.

Batch Sizing: How Big a Bite?

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.

Recognizing a List Bug When You See One

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.

Safety Rails Against Runaway Loops

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:

  1. Bound the input before the loop. Put a filter, a Limit node, or an explicit size check upstream of the split. If more than some sane maximum of elements arrives, stop and notify a human instead of processing — Chapter 20 (Humans in the Loop) shows the approval patterns. Zapier's iteration cap acts as a crude built-in bound; treat Make's scenario-level cycle and execution settings, and n8n's Limit node, as your deliberate ones.
  2. Break the self-trigger circuit. Never let a workflow write unfiltered into its own trigger's source. If it must, mark the records it writes (a tag, a status field) and filter its trigger to ignore marked records. This one rule prevents the most expensive incidents in the genre.
  3. Test on a sample, not the firehose. Pin a small test input (n8n lets you pin data to a node; on Make and Zapier, run against a copy or a deliberately small record) and inspect counts before pointing the workflow at real volume. Chapter 26 (Testing, Debugging, and Launch) makes this a discipline.
  4. Alert on volume, not just failure. A runaway loop often succeeds, run after run. Set up monitoring that notices "this workflow executed 400 times today against a usual 12" — Chapter 27 covers history and alerting — and know where each platform's off switch is before you need it.
  5. Prefer batched writes to per-element writes. Aggregating fifty records into one API call is not just cheaper; it shrinks the runaway surface from fifty opportunities to one.

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.

Where This Leaves You

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?"


Storing Data Inside the Platform

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).

Why Workflows Need Memory

Five recurring situations force a workflow to remember something between runs:

  1. "Have I seen this before?" A form gets submitted twice, an API returns overlapping pages, a webhook fires on retry. Without a memory of what was already processed, your workflow sends the same welcome email twice or creates duplicate invoices.
  2. "What does this code mean?" An order comes in with 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.
  3. "Deal with this later." Items arrive faster than a downstream system can accept them, or you want to batch fifty small events into one summary. You need a holding pen — a queue.
  4. "How many so far?" Daily send caps, monthly usage tallies, "escalate after three failures" rules. All of these are counters.
  5. "Where did I leave off?" A scheduled workflow that fetches "everything new since last time" needs to record a bookmark — usually the timestamp or ID of the last item it handled — so the next run starts in the right place.

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.

The Built-In Stores, Platform by Platform

Zapier: Storage by Zapier and Zapier Tables

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: Data Stores and Data Structures

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: Data Tables and Workflow Static Data

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.

The stores at a glance

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

The Canonical Patterns

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.

Deduplication keys: remembering what was already processed

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."

Lookup and enrichment tables

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.

Simple queues

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).

Counters and rate windows

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 + 1 is atomic by nature.

Watermarks: passing state between runs

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.

Limits, Races, and Other Sharp Edges

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.

When to Graduate: Sheets, Airtable, or Postgres

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.

Choosing in Practice

A compact decision path, in the spirit of Chapter 4 (The Rosetta Stone, the Capability Map, and How to Decide):

  1. Is it one of the five patterns, at operational scale, consumed only by workflows? Use the built-in store: Tables (or Storage for counters and bookmarks) on Zapier, a data store with natural keys on Make, Data Tables with upsert (plus static data for watermarks) on n8n.
  2. Will a human maintain or review it weekly? Prefer the stores with editing screens — Zapier Tables, Make data stores, n8n Data Tables — and design the table for the human, not just the workflow.
  3. Did any graduation criterion trigger? Move that dataset out — Sheets or Airtable when people are the point, Postgres when scale, concurrency, or reporting is — and let the workflow read and write it like any other app.
  4. Either way: schedule the janitor, advance watermarks only on success, and keep every write idempotent.

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.


Files and Documents

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).

What a File Is to an Automation Platform

Strip away the icon and the preview, and a file is three things:

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.

How Each Platform Carries a File

This is the core mental model of the chapter. Get this straight and the rest is details.

Zapier: file objects and hydration

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:

  1. File fields accept URLs. If you map a plain, publicly reachable URL into a field that expects a file, Zapier will download whatever that URL serves and treat it as the file. This is the standard way to move a file into Zapier from any app that exposes a download link.
  2. File references are temporary. The pointer is valid during the run and for a limited window afterward, not forever. It is a courier's tracking number, not an archive.

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: bytes in the bundle

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: the binary tab and named properties

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.

The model at a glance

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

Moving Files Between Apps

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.

Acquiring the file

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.

Uploading and the mapping trap

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 .pdf name. It looks fine in the folder listing and is garbage when opened. Zapier is the exception: its file fields deliberately accept URLs and fetch them. Do not carry that habit across platforms.

When you can skip the bytes entirely

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 Attachments

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.

Parsing CSVs into Records

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:

The format's traps are universal and worth naming because every one of them produces silently wrong data rather than an error:

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.

Extracting Text from PDFs

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.

Converting Between Formats

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.

The Limits That Ambush You

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.

Worked Example: Invoice In, Filed, and Logged

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.

The Zapier build

  1. Trigger: Gmail's new-attachment trigger, scoped to the AP mailbox or a label. One run per attachment means multi-invoice emails are handled for free.
  2. Filter: continue only if the attachment's name ends in .pdf — this stops signature images cold.
  3. Extract: send the file object to a template-based parser app configured with one template per supplier layout; it returns invoice number, vendor, amount, and date as plain fields. (Zapier's own email parser reads message bodies, not PDF attachments — for PDFs you want a document parser from the catalog.)
  4. File it: a Google Drive upload action. Map the trigger's file object into the file field — hydration fetches the bytes — and build the filename from the extracted fields: {{date}}-{{vendor}}-{{invoice_number}}.pdf.
  5. Log it: a Google Sheets create-row action mapping the extracted fields plus the Drive link returned by step 4.

Each invoice consumes a handful of tasks. Simple, robust, and the parser subscription is the moving part to watch.

The Make build

  1. Trigger: the email watch module on the AP mailbox; each new message arrives as one bundle with an attachments array.
  2. Iterate: an Iterator pointed at the attachments array fans out one bundle per file; a filter on the route keeps only names ending .pdf.
  3. Extract: hand the file to a document-parsing or conversion service module for text, then — if the service returned raw text rather than fields — a text-parser module with a pattern per field (Invoice\s*#?\s*(\d+) and friends). toString() is your bridge anywhere a module hands you binary where text is needed.
  4. File it: the cloud-storage upload module, mapping the attachment's 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.
  5. Log it: the spreadsheet add-row module, mapping extracted fields plus the returned file link.

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.

The n8n build

  1. Trigger: the Gmail trigger with attachment download enabled; each email is one item, PDFs riding as attachment_0, attachment_1, …
  2. Normalize: split attachments so each becomes its own item, then an IF node keeps items whose MIME type is application/pdf.
  3. Extract: Extract from File in PDF mode reads the binary property and outputs the document text as a JSON field — natively, no external service, because these are text-layer PDFs. A small Code node applies the same regular expressions to produce clean invoice_number, vendor, amount, date fields.
  4. File it: the Google Drive node's upload operation, pointed at the correct input binary property (check the Binary tab if unsure — it is always the first thing to check) with the filename built by expression.
  5. Log it: the Google Sheets node appends the row with the Drive link.

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.

What the three builds teach

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.