n8n Node UX: loadOptions, Resource Locator and Naming
In this video
This is the written version of Part 6 of Build your own n8n node. Put two nodes side by side. Both work. One is ours: a blank Contact ID box, operations called List and Fetch, fourteen fields stacked down the panel. The other is the kind n8n ships: a searchable picker, two fields, space. You can feel the difference in a second, even if you can't name it.
It comes down to a handful of small things, and none of them is hard. They matter twice: once for your users, and again when you ask n8n to verify the node, because a person checks most of them against n8n's UX guidelines.
A list that fills itself
Start with that empty Contact ID box. Nobody knows their contact IDs. Right now your user has to leave n8n, find the contact in your app, copy the ID, come back and paste it. Every time.
So fetch the list for them. Add a loadOptions method to the node class:
methods = {
loadOptions: {
async getContacts(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const page = await acmeRequest.call(this, 'GET', '/contacts', undefined, { limit: 100 });
return (page.data as IDataObject[]).map((contact) => ({
name: contact.email as string,
value: contact.id as string,
}));
},
},
};
Then make the field use it:
{
displayName: 'Contact Name or ID',
name: 'contactId',
type: 'options',
typeOptions: { loadOptionsMethod: 'getContacts' },
default: '',
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
},
The user sees email addresses. Your API gets IDs. That box is now a dropdown of their real contacts, from their own account. It's about ten lines, and it's probably the biggest single improvement you can make to a node. (The display name and description follow the wording n8n's linter expects for dynamic dropdowns.)
Three ways to pick: the resource locator
Look closer at n8n's own nodes and they offer three ways to pick a record: from a list, by ID, or by pasting a URL. It seems like overkill until you think about who's using it.
- One person has two thousand contacts and wants to type a few letters and search.
- Another already has the ID, because an earlier step in the workflow produced it.
- Someone else has the contact open in another tab and wants to paste the link.
That's the resourceLocator field:
{
displayName: 'Contact',
name: 'contactId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
required: true,
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
typeOptions: { searchListMethod: 'searchContacts', searchable: true },
},
{
displayName: 'By ID',
name: 'id',
type: 'string',
placeholder: 'e.g. c_4821',
},
{
displayName: 'By URL',
name: 'url',
type: 'string',
placeholder: 'e.g. https://app.acme.example/contacts/c_4821',
extractValue: { type: 'regex', regex: 'contacts/([a-zA-Z0-9_]+)' },
},
],
},
The list mode needs a search method, under methods.listSearch:
listSearch: {
async searchContacts(this: ILoadOptionsFunctions, filter?: string): Promise<INodeListSearchResult> {
const page = await acmeRequest.call(this, 'GET', '/contacts', undefined, { search: filter, limit: 50 });
return {
results: (page.data as IDataObject[]).map((contact) => ({
name: contact.email as string,
value: contact.id as string,
url: `https://app.acme.example/contacts/${contact.id}`,
})),
};
},
},
In execute(), ask for the plain ID whatever mode was used:
const contactId = this.getNodeParameter('contactId', i, '', { extractValue: true }) as string;
It's more code than a plain dropdown. It's also the thing that makes a node feel like the built-in ones, and n8n's guidelines ask for it whenever the user selects a single item, with From List as the default mode.
The words
The part most people skip. Our operations are List, Fetch and Remove. Perfectly good words, and not n8n's words.
n8n's vocabulary is Create, Create or Update, Delete, Get, Get Many and Update. Not "List", not "Get All": Get Many.
That sounds picky. Here's why it isn't: your user has already used fifty other nodes, and every one of them says Get Many. When yours says the same, they already know how to use it.
Each operation has three pieces of text, and each has its own rule:
| Field | Shown | Case | Example |
|---|---|---|---|
name |
in the Operation dropdown | Title Case, no resource | Get Many |
action |
in the nodes panel search | sentence case, with the resource, no articles | Get many contacts |
description |
under the name in the dropdown | sentence case, with the resource | Retrieve a list of contacts |
A few more from the same guidelines:
- Field labels are Title Case. Descriptions and hints are sentence case.
- A boolean's description starts with "Whether".
- Placeholders start with "e.g.", like
e.g. nathan@example.com. - Use your product's own words from its interface, not the API's internal names.
Don't show everything: Additional Fields
Our Create Contact has fourteen fields. Two are required. Opened, it looks like a tax form.
Move the optional ones into a collection, which n8n shows as Additional Fields with an Add Field button:
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: { show: { resource: ['contact'], operation: ['create'] } },
options: [
{ displayName: 'First Name', name: 'firstName', type: 'string', default: '' },
{ displayName: 'Last Name', name: 'lastName', type: 'string', default: '' },
{ displayName: 'Phone', name: 'phone', type: 'string', default: '' },
],
},
Now the node opens with two fields and a button. Same node, same power, and the first thing the user feels is "this is easy."
Three small things
The icon. Use your real logo as an SVG with a square canvas, set as icon: 'file:acme.svg'. People find nodes by the picture before they read the name.
The subtitle. The line under the node on the canvas. This makes it say what the node is doing, like "create: contact":
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
Delete. When a delete works, most APIs send back nothing. If your node passes nothing on, the next node has no items, and it doesn't run. n8n's guidelines say to return one item, { "deleted": true }:
returnData.push({ json: { deleted: true }, pairedItem: { item: i } });
That last one catches a lot of people, because everything looks fine when you test the node on its own.
One more from the guidelines, for later: if an endpoint returns more than ten fields, add a Simplify toggle that returns the most useful ten.
The checklist
None of this is hard. The hard part is remembering all of it, for every operation, every time:
- dropdowns that load the user's own data
- a resource locator wherever a single record is picked
- n8n's operation names, in the right case
- optional fields under Additional Fields
- your real icon, and a subtitle
{ "deleted": true }from every delete
Keep n8n's UX guidelines open in a tab while you work.
Now the node is ready, and it exists on one laptop. In Part 7 it goes on npm, and a clean n8n that has never heard of you installs it by name. On the way is a publishing rule that catches most people the first time.
One more thing. NativeShip, the tool we build, generates nodes in this structure from an OpenAPI spec, and its editor has a live n8n preview, so you can see every operation name the way a user will and fix any that came through in your API's words before you build.
Frequently asked questions
Add a method under methods.loadOptions that calls your API and returns { name, value } pairs, then point a field of type options at it with typeOptions: { loadOptionsMethod: 'yourMethod' }.
A field type, resourceLocator, that lets the user pick a record from a searchable list, by ID, or by pasting a URL. n8n's UX guidelines ask for it whenever the user selects a single item, with From List as the default.
Use n8n's vocabulary: Create, Create or Update, Delete, Get, Get Many and Update. The operation name is Title Case and doesn't repeat the resource; the action is sentence case, includes the resource and drops articles, like "Create contact".
A single item, { "deleted": true }. If the node returns nothing, the next node in the workflow has no items and doesn't run.