Reshape the data between two nodes without writing a line of code
The node upstream hands you a large blob of JSON. The node downstream wants three fields out of it, renamed, combined, and with a default filled in where one is missing. That gap is where beginners reach for a Code node and where they do not need to. The Set node — called Edit Fields (Set) in the newer interface — is the visual tool built for exactly this job, and learning it properly is what keeps most of your workflows free of JavaScript.
The node that changes nothing except the shape
Part 7 of this series established the two facts everything here rests on: items are the unit that flows through n8n, and every item is a blob of JSON. Wire up a couple of SaaS nodes and you hit the consequence almost immediately — the two ends of a connection rarely agree about what that blob should look like.
A concrete version of the mismatch. The HTTP Request node hands back everything the API knows about a person:
{ firstName: "Xiaoming", lastName: "Wang", email: "...", age: 28, avatar_url: "...", ... }{ Name: "Wang Xiaoming", Email: "...", Age: 28 } — three fields, one of them assembled from two others.You could hand-assemble ten lines of expression inside the Google Sheets node itself. You could open a Code node and write JavaScript for it every time. Neither is a good habit. What the workflow wants instead is a waypoint: one node between the two whose only job is to comb the upstream JSON into the shape the downstream node is asking for.
It is the counter between the shopping bags and the fridge. Nothing gets cooked there and nothing leaves the house. Things come out of the bag, get sorted, and go onto the right shelf in a form you can find later. Every bit of the work is rearrangement.
That is the Set node, and the “nothing leaves the house” part is worth stating precisely, because it is what makes the node safe to experiment with. It calls no API. It triggers no webhook. It has no effect on the outside world at all. It takes JSON, gives it a new shape, and passes it on.
Internally it is a small machine with three steps, and it repeats them for every item that arrives.
flowchart LR A["Previous node"] --> B["Read items"] B --> C["Rebuild JSON"] C --> D["Emit items"] D --> E["Next node"] X["No API call.
No webhook.
No side effect"] -.-> C
It runs once per item automatically, it is one of the fastest nodes in the product, and it usually finishes in milliseconds.
set or edit fields finds it. Recognize the shape rather than the word — your build may label it differently again.Manual Mapping or JSON, decided before you start
Open the node and the first thing at the top is a Mode dropdown with exactly two options. Everything else in the node changes depending on which one you pick.
| Mode | How you set it up | When to use it |
|---|---|---|
| Manual Mapping (the default) |
Add one field at a time with Add Field: a Name (what the new field is called), a Type (String / Number / Boolean / Array / Object) and a Value, which can be an expression or a fixed value | Around 90% of cases. Adding a few fields, renaming a few, filling in a few defaults — the visual interface is the easiest to read back later |
| JSON | Paste a whole JSON template and fill the values in dynamically with {{ }} expressions inside it |
You have to restructure the JSON — turning flat data into nested data, say — or you already have a template to copy |
Underneath, both modes do the identical thing: produce a new piece of JSON. The only real difference is whether you build it in a visual interface or type the JSON text yourself. If you are new to the node, start on Manual Mapping every time and switch to JSON only when you need a nested structure.
flowchart TD
A["You need to change the shape"] --> B{"Does the output have to be nested,
or do you already have
a JSON template?"}
B -->|"no"| C["Manual Mapping
add one field at a time"]
B -->|"yes"| D["JSON mode
paste the whole template"]
C --> E["Decide before you start.
Switching modes later
clears what you set"]
D --> E
Combining two fields into one, step by step
Joining a first and last name into a single field is the most typical thing a Set node ever gets asked to do, so it makes the best first build. Assume the node upstream produces one item shaped like this:
| Field | Value | Type |
|---|---|---|
firstName | "Xiaoming" | String |
lastName | "Wang" | String |
email | an address | String |
age | 28 | Number |
You want one more field on the end called full_name, with the value Wang Xiaoming — last name first, because that is what this example's required output asks for. Choose an order and a separator that match your own users' naming conventions rather than copying this one blindly.
-
Step 1
Add an Edit Fields node after the one producing that JSON
On the canvas press + to add a node, search for
edit fieldsorset, and pick Edit Fields (Set). Wire it directly after the node whose output you just looked at. -
Step 2
Leave Mode on Manual Mapping
Manual Mapping is the default, so there is nothing to change here. You are adding one flat field, which is exactly what this mode is for.
-
Step 3
Click Add Field under Fields to Set
A blank field card appears underneath. Set its Type to String, because a name is a string.
-
Step 4
Put full_name in the Name box
This is the new field's name, and it is the exact name every downstream node will see in the JSON. Spell it the way you intend to refer to it later.
-
Step 5
Put the expression in the Value box
There is an Expression toggle at the top right of the Value field. Switch to Expression mode and paste this in:
{{ $json.lastName }}{{ $json.firstName }}Two expressions, side by side, with nothing between them.
$jsonmeans the JSON of the item being processed right now — Part 10 of this series covers the syntax in full.Read that last sentence literally, because it matters here. With nothing between the two expressions there is no space between the two values either. Exactly as printed above,
full_namecomes out asWangXiaoming— while the value asked for at the top of this section, and the output shown in Step 7, both readWang Xiaoming, with a space. Those two do not agree. The expression is reproduced here character for character as the original tutorial gives it rather than quietly corrected, because the mismatch is the useful part: nothing supplies the separator for you. You type it, or it is not there.If you want the space, put a literal space between the two pairs of braces:
With a space{{ $json.lastName }} {{ $json.firstName }}A comma and a space go in the same position; so does nothing at all, if the names you handle are written closed up. This is the point in the build where you choose the separator your own users' naming conventions want, and whichever you choose, every step after this one behaves identically.
-
Step 6
Tick Include Other Input Fields
Further down the node there is a toggle labeled Include Other Input Fields. Ticked, it keeps every original field and adds the one you just set. Unticked, it keeps only what you set and throws the rest away. Tick it here — the next section is entirely about this toggle, because it causes more confusion than the rest of the node put together.
-
Step 7
Run Execute Node and read the output
Press Execute Node at the top right of the node. The Output panel should show every original field still present, plus the new one:
{ firstName: "Xiaoming", lastName: "Wang", email: "...", age: 28, full_name: "Wang Xiaoming" }That is the output with a separator in the expression. Paste Step 5's expression exactly as it is printed and the same run gives you
full_name: "WangXiaoming"instead — right in every other respect, and one keystroke away from either version. Read the Output panel rather than assuming; that is what it is for.
{{ $json.xxx }} for you. You can also drag a field straight from the input panel on the left into the Value box, which is the least-effort route of all when you are new.The five things Set gets used for most
Learn these five and you have covered the large majority of everyday Set node work. All of them are within reach of Manual Mapping — no Code node, nothing to install.
| What you want | How to set it | Example value |
|---|---|---|
| Add a new field (a timestamp, a status flag) |
Add Field, put the new name in Name, produce the value with an expression | {{ $now }} for the current time; Processed as a plain string |
| Rename a field | Add Field with the new name, pull the old value across; then turn Include Other Input Fields off, or list every field you want to keep | Name customer_email, Value {{ $json.email }} |
| Fill in a default (when the field is missing) |
Test for a value inside the expression itself | {{ $json.phone || 'not provided' }} |
| Convert a type (string to number and the like) |
Wrap it in a built-in JavaScript function | {{ Number($json.amount) }}, {{ String($json.id) }}, {{ Boolean($json.is_vip) }} |
| Fill a value conditionally | Use a ternary operator — condition, then the value if true, then the value if false | {{ $json.amount > 1000 ? 'VIP' : 'Standard' }} |
Most of the built-in JavaScript functions can be written directly in an expression: Number, String, Boolean, Math.round, Date, and string methods such as .toUpperCase(), .trim() and .split().
Include Other Input Fields, and why the docs disagree with your screen
This one toggle is responsible for more confused half hours than anything else in the node, so it gets a section of its own. Its behavior is not complicated. It is just that the consequence of getting it wrong shows up three nodes later, dressed as a different problem.
Ticked, you are writing on a photocopy of the form — everything already filled in stays, and your changes go on top. Off, you are typing a fresh blank form and only the boxes you actually type into exist. The address you did not retype is not blank on the new form. It is simply not on it.
| Toggle state | Behavior | What ends up in the output |
|---|---|---|
| Include Other Input Fields ticked |
Keeps every field the input had; the fields you set overwrite the ones with the same name, or are added when the name is new | The original 5 fields + the 2 you added = 7 fields |
| Include Other Input Fields off |
Keeps only the fields you listed under Fields to Set, and throws everything else away | You set 2 fields = 2 fields |
flowchart TD A["Input item
5 fields"] --> B{"Include Other Input Fields"} B -->|"ticked"| C["The 5 original fields
plus the 2 you set"] C --> D["7 fields go downstream"] B -->|"off"| E["Only the 2 fields listed
under Fields to Set"] E --> F["2 fields go downstream.
The other 3 are gone"]
Three situations, three answers:
- Adding or changing fields while keeping the rest. Tick it. This is the common case and the one you will use most days.
- A deliberate clean-out. You want exactly three fields to go downstream and nothing else — turn it off, then list those three fields under Fields to Set.
- Security. The upstream API response holds fields you do not want carried any further, such as a token or a
password_hash. Turn the toggle off and let through only the safe ones.
email under Fields to Set, and no downstream node will ever see an email again. When a field is missing from an output panel, check this toggle before you check anything else. One further misreading worth heading off: the toggle only carries the fields of the current input item. To reference the payload of a node several steps back you still need {{ $('Node Name').item.json.xxx }}, whatever this toggle is set to.Why the official docs say something else
In the older interface this toggle was called Keep Only Set Fields, and its logic was exactly reversed: ticked meant keep only what you set. From v1 it is Include Other Input Fields, where ticked means keep the others — which reads far more naturally, and which is why the change was made.
The wrinkle is that the official n8n documentation page still says Keep Only Set Fields to this day. So a docs page that does not match the wording on your screen is entirely normal here, and it is not a sign that you are looking at the wrong node. Go by what the UI in front of you says, and read the logic off the label rather than assuming it matches something you read elsewhere.
When the job is restructuring rather than tweaking
Manual Mapping gets clumsy the moment the output has to be nested. Say the upstream node gives you four flat fields — id, firstName, lastName, amount — and the API you have to call wants an orderId at the top level with a customer object beneath it holding a name and a vip flag. Building that one line at a time is where people get lost.
It is the difference between describing a room over the phone and drawing the floor plan. Both get the furniture in the right places eventually. Only one of them lets you glance at it afterwards and see that the sofa is blocking the door.
JSON mode settles it with a single paste.
-
Step 1
Switch Mode to JSON
Pick JSON in the Mode dropdown at the top of the node. Fields to Set disappears and a large text box takes its place.
-
Step 2
Paste the template, expressions and all
This is the whole template, written on one line so you can copy it exactly as it stands:
{ "orderId": "{{ $json.id }}", "customer": { "name": "{{ $json.lastName }}{{ $json.firstName }}", "vip": {{ $json.amount > 1000 }} } }Note the quoting, because it is the part that goes wrong. String values have to be wrapped in quotes —
"{{ ... }}"— while boolean and numeric ones must not be.{{ $json.amount > 1000 }}evaluates to a baretrueorfalse, and quoting it would turn a boolean into the string"true". Thenameline is the same two-expression join as Step 5 of the previous section, separator and all — written like this it produces no space, and a space typed between the two pairs of braces produces one. -
Step 3
Decide the Include toggle again
JSON mode has the same Include Other Input Fields toggle. Most of the time you are in JSON mode precisely because you are restructuring, so you turn it off and keep only the fields the template defines.
-
Step 4
Run Execute Node and check the structure
The Output panel shows the structure you pasted with every expression already filled in with real values. Once the shape is right, wire up the downstream
HTTP Requestnode and make the call.
Worked example: a nested API response into one sheet row
Now put the pieces together into something you would actually build. You called an order API with HTTP Request, what came back is nested three levels deep, and it has to be appended to an order-tracking sheet in Google Sheets as a single row.
The response looks like this, written as paths so the expressions are easy to read off:
| Path in the response | Value | Sheet column it feeds |
|---|---|---|
data.order.id | "ORD-9527" | Order ID |
data.user.name | "Wang Xiaoming" | Name |
data.user.email | an address | |
data.order.amount | 2800 | Amount |
data.meta.ts | "2026-08-16T10:30:00Z" | Timestamp |
| — derived from the amount — | "VIP" or "Standard" | VIP |
The sheet's header row is Order ID | Name | Email | Amount | Timestamp | VIP. Six columns, so six fields.
flowchart LR A["HTTP Request
nested response"] --> B["Edit Fields (Set)
Manual Mapping
Include Other Input Fields off"] B --> C["6 flat fields named
like the sheet header"] C --> D["Google Sheets
Append one row"]
Click Add Field six times and fill them in against the header, column by column:
| Name | Type | Value (expression) |
|---|---|---|
| Order ID | String | {{ $json.data.order.id }} |
| Name | String | {{ $json.data.user.name }} |
| String | {{ $json.data.user.email }} | |
| Amount | Number | {{ $json.data.order.amount }} |
| Timestamp | String | {{ $json.data.meta.ts }} |
| VIP | String | {{ $json.data.order.amount > 1000 ? 'VIP' : 'Standard' }} |
Run Execute Node and the output should be six purely flat fields, with no data wrapper left anywhere. Then wire up a Google Sheets Append node and set its Data Mode to Map Each Column Below. Because the field names already line up exactly with the header, you can drag the fields from the left panel straight into the matching boxes — no expressions to type a second time.
That naming trick is the point of the whole exercise. Sheets, Slack, Notion and Airtable are all fussy about field formats, and a Set node that hands them fields already named the way they want them turns a fiddly configuration into drag and drop.
Three nodes can reshape data. Only two are still on offer
If you have followed an older tutorial you will have met the Function node, and it is worth sorting the three out once so you never wonder again.
| Node | Any code? | What it can do | When to use it |
|---|---|---|---|
| Set (Edit Fields) |
No | Add, change and drop fields; simple expressions; type conversion | Around 90% of data shaping. Reach for this first — if you can avoid writing code, avoid it |
| Function | You write JavaScript | Loops and a few helpers; more flexible than Set, still limited | Do not use it any more. It was merged into Code in v0.198.0 and is gone from the nodes panel; you only see one when you import an old workflow |
| Code (the current one) |
You write JavaScript or Python | Full program logic — loops, if / else, npm modules, complex calculations | Logic that is genuinely too complex for Set. Part 15 of this series covers it |
Writing a Code node to add one field is renting a van to carry one bag. It gets the bag there. It also means you are now responsible for a van — the errors it can throw, and the colleague who inherits it and cannot read JavaScript.
The question to ask, every time, is simply: can Set do this? If it can, use Set. Move up to Code only when it cannot. Set is visual, easy to read back, hard to get wrong and quick to run; Code can do more, but you write it, you handle its errors, and a non-engineer who inherits the workflow has a much harder time with it.
function will not turn one up any more — only Code. Use Code for anything new.Four tricks worth knowing
1 · Nested fields with dot notation, without leaving Manual Mapping. Manual Mapping can produce a nested structure too. The trick is to build a path in the Name box using dots — set customer.name to {{ $json.firstName }} and customer.email to {{ $json.email }}, and the output is a customer object with those two keys inside it. It saves switching modes for a shallow nest.
2 · Many items in one pass, with no loop. Like every other node, Set runs once per item automatically. Send ten items in and you get ten reshaped items out. On each pass $json points at the item being processed right then, which is why the same single expression works for all of them.
3 · Continue On Fail, so one broken item does not stop everything. The Settings gear at the top right of the node holds Continue On Fail. When an expression fails on one item — a field that does not exist, so the path resolves to nothing — and this is off, the whole workflow stops in red. With it on, that item is marked as an error and the workflow keeps running: you can see in the output which items broke while the rest carry on downstream.
4 · A Set node at the front is a fake-data generator. No real trigger built yet, but you want to test the nodes after it? Drop in a Manual Trigger and a Set node, put the Set node in JSON mode, paste a block of fake JSON and turn Include Other Input Fields off. Pressing Execute Workflow now sends one made-up item down the whole chain. It is a lifesaver while debugging.
Code node and delete item.json.fieldName.Eight things that go wrong, and what each one actually is
Almost every Set node problem announces itself the same way — something downstream is empty — and the cause is one of a small number of things. Work through them in this order rather than at random.
flowchart TD A["A field is missing
after the Set node"] --> B{"Is Include Other Input Fields
ticked?"} B -->|"no"| C["Tick it, or add that field
by hand under Fields to Set"] B -->|"yes"| D{"Does the output panel
show nothing for it?"} D -->|"yes"| E["The path is wrong. Run the upstream
node and copy the path
from its real output"] D -->|"no"| F{"Did you rename it?"} F -->|"yes"| G["Update every downstream expression
to the new name"] F -->|"no"| H["Check the Type and the quoting.
A number can arrive as a string"]
| Symptom | Cause | How to fix it |
|---|---|---|
Fields vanish after Set — email was there and the downstream node cannot get it |
Include Other Input Fields is not ticked (or the old Keep Only Set Fields is on), and you did not list email under Fields to Set either |
Turn the toggle on, or Add Field by hand for every field you want to keep |
| An expression comes back undefined | The field path is wrong — upstream is really $json.name and you wrote $json.data.user.name |
Run Execute Node on the upstream node first, look at what the JSON in its output panel actually is, and copy the path from there; or click the field name in the input panel on the left and let n8n fill it in |
| You added a field but the downstream node does not get it | The downstream expression still refers to the old name — you renamed email to customer_email and downstream still asks for $json.email |
After a rename, update the expressions in every downstream node to match. Renaming has knock-on effects |
| The field name has a space in it and the expression cannot find it | JavaScript dot notation does not support spaces, so $json.field name cannot work |
Use the bracket form: {{ $json['field name'] }}, {{ $json['full name'] }}. Brackets take any character |
You clearly typed a number, but the output is the string "123" |
The Type under Fields to Set is String, or the value is wrapped in quotes in JSON mode | Manual Mapping: change Type to Number. JSON mode: leave the quotes off, "amount": {{ $json.amount }}. Or convert it, {{ Number($json.amount) }} |
| Every field disappeared after switching modes | Manual Mapping and JSON mode do not share settings, and switching clears them | Ctrl+Z to undo back to the original mode; or decide on the mode before you start setting fields |
| JSON mode shows Invalid JSON in red | A string field is missing its quotes, or there is one comma too many or too few | Paste it into a JSON validator such as jsonlint.com to find the character. A {{ }} expression also has to sit in a legal JSON position, and string ones need their quotes: "{{ ... }}" |
| The interface gets sluggish once Manual Mapping has a pile of fields | Rendering slows down when a Set node has too many fields — more than 50 or so | Switch to JSON mode. Fifty keys are a small clump of text in a JSON box and far smoother than fifty visual cards |
Questions that come up
Does adding Set nodes slow a workflow down?
HTTP Request and the SaaS nodes.How many fields can one Set node hold?
When exactly should I move up to a Code node?
map / filter / reduce over an array; or you need logic that references items against each other, such as folding ten items into one. Part 15 covers the Code node.Why was Set renamed to Edit Fields?
Can Set reshape an array field, turning tags into a single string?
tags, Type String, Value {{ $json.tags.join(', ') }} gives you one comma-separated string. The other direction, splitting a string into an array, is {{ $json.csv.split(',') }} with Type set to Array. Set can do any array work inside a single item; flattening or merging the items array itself — one item becoming three, or three becoming one — needs the Split In Batches or Merge nodes from Part 11, or a Code node.What timezone is $now in, and will it drift once it reaches Sheets?
$now is an n8n Luxon DateTime object and defaults to the n8n instance's timezone. Verify that timezone in your workflow settings rather than assuming a particular region. Written straight into Sheets, Sheets sometimes decides to be clever and shifts it. The reliable approach is to produce a string yourself: put {{ $now.toFormat('yyyy-MM-dd HH:mm:ss') }} in Value, set Type to String, and format that column in Sheets as plain text first. Then both the timezone and the format are under your control.With the toggle ticked, what happens if a field I set has the same name as an existing one?
email and you Add Field with Name email and a different value, the output carries your value. There is no error and no warning. That makes it a neat way to change a value in place — masking a sensitive field, or normalizing a column with something like {{ $json.email.toLowerCase() }}. On a collision, the value you set always wins.Can a Set node reference a node other than the previous one?
{{ $('Node Name').item.json.field }} in the Value box to pull data from any earlier node, not just the one immediately behind. Say a workflow opens with a Webhook node named Webhook, three nodes run after it, and the last Set node needs both the webhook's original payload and the result computed one step earlier — then Value can be {{ $('Webhook').item.json.customer_id }} - {{ $json.result }}. This comes up constantly in longer workflows, and it is far cleaner than adding a Set node at every step just to keep a field alive. Part 10 has the full expression examples, and the sub-workflows in the next part use a similar technique.Where to go from here
You can reshape data. Next: stop repeating yourself.
Part 14 takes the same instinct one level up. Once three workflows all contain the same five nodes, that sequence wants to become a sub-workflow with a defined input and a defined return — called from anywhere, fixed in one place.
Open the full guidePart 13 of the Woow n8n Onboarding Guide series on the Apporo blog.
Adapted from the Woow n8n Onboarding Guide, produced by WoowTech and released under CC BY 4.0. This adaptation is published by Apporo under the same licence.
Light · Air · Water · Control · apporo