Skip to content
Blog

How to Map API Responses in a Declarative n8n Node

An API response rarely arrives in the shape a workflow should consume. A list may be wrapped in data, a provider may return inactive records that the node should hide, or a dropdown endpoint may need { name, value } options instead of the provider's original fields.

A declarative n8n node can handle many of these changes in routing.output.postReceive. Use the built-in actions to extract a nested property, filter records, create key-value objects, sort results, or replace the response with a deliberate output shape. Keep the actions beside the operation that owns the response.

Scope the post to response shaping. Request construction, credentials, and complex programmatic transformations stay in their own boundaries. The NativeShip declarative node tutorial covers the full node structure; this post focuses on the output pipeline after the API call returns.

The pipeline runs top to bottom, and each action receives the output of the one before it:

Step Action Input Output
1 rootProperty { ok, data: [...], next } One item per record in data
2 filter Every record Only records where active is true
3 setKeyValue Active records { name, value } objects
4 sort Option objects Options in alphabetical order

Start with the response contract

Write down the response shape before choosing a postReceive action. The important questions are:

Question Example answer Mapping consequence
Where are the records? body.data Extract data with rootProperty
Is the result always an array? The provider returns one object for a single record Test the object case before assuming a list
Which records should pass? Only records with active: true Use filter or keep the provider's full result
Which fields should users see? name and id Use setKeyValue for option-style output or keep the full object
Does order matter? Users expect alphabetical options Add sort after the output fields exist
Does the node need metadata? A create call returns an ID and status Use a set expression only when the replacement shape is intentional

The current n8n declarative-style parameter reference shows output.postReceive for a load-options request. The declarative node tutorial shows how that routing sits inside the wider node structure. The same response actions can clarify an operation's output when the action is supported by the node's current n8n version.

Extract a nested array with rootProperty

Suppose an endpoint returns:

{
  "ok": true,
  "data": [
    { "id": "a1", "name": "Alpha" },
    { "id": "b2", "name": "Beta" }
  ],
  "next": null
}

The workflow usually needs the records, not the envelope. Put rootProperty first:

routing: {
  request: {
    method: 'GET',
    url: '/records',
  },
  output: {
    postReceive: [
      {
        type: 'rootProperty',
        properties: {
          property: 'data',
        },
      },
    ],
  },
},

The action selects the data property and turns its contents into output items. If the selected value is not an array, n8n's routing implementation treats it as a single item. That makes the action useful for a provider that returns one record for a get operation and a list for a collection operation, but you still need to test both shapes and decide whether they should share one operation.

Do not guess the envelope name from a similar API. Confirm whether the provider uses data, items, results, records, or a resource-specific key. A correct request with a wrong root property can look like an empty node output.

Filter records after extraction

Once each record is the current response item, a filter action can remove records that should not appear in the node output:

{
  type: 'filter',
  properties: {
    pass: '={{$responseItem.active === true}}',
  },
},

Place this action after rootProperty when the flag lives inside each record. The expression is evaluated for each response item. Test true, false, missing, and unexpected values. If the provider uses strings such as 'active' and 'archived', compare that documented value instead of relying on JavaScript truthiness.

Filtering changes the number of output items. That is a product decision for the node: a user who chooses "Get all" may expect inactive records too, while a load-options field may need only selectable records. Name the operation and field so the behavior is visible in the editor.

Cap the number of results with limit

The limit action keeps the first N items, which is useful for a "Return All" toggle paired with a "Limit" field:

{
  type: 'limit',
  properties: {
    maxResults: '={{$parameter.limit}}',
  },
},

Prefer a provider-side limit (a limit or per_page query parameter) when the API supports one, because it saves bandwidth and rate-limit budget. Use the limit action when the API ignores the parameter or when filtering has already removed some records. If the user asks for all results across pages, combine the operation with declarative pagination (routing.operations.pagination) rather than a large single request. The pagination guide covers cursor, offset, and link-header patterns and when to move them into code.

Turn records into key-value options

Load-options and resource-locator fields often need a label and an internal value. The loadOptions and resource locator guide covers the field side; on the response side, after selecting the array, use setKeyValue:

{
  type: 'setKeyValue',
  properties: {
    name: '={{$responseItem.name}}',
    value: '={{$responseItem.id}}',
  },
},

This creates option-shaped records from provider fields. Keep this transformation for a field that expects options or for a deliberate normalized output. A general "Get many" operation may be more useful when it returns the provider's complete record fields, including the ID, status, timestamps, and links.

If the provider's label is nested, use the documented path in the expression. If the ID can be empty, add a filter or validation rule before exposing the option. Do not hide a missing identifier by using the display name as a fallback unless the API contract says that name is stable.

Sort only after the output fields exist

Sorting is easiest to reason about after the list has the field users will see:

{
  type: 'sort',
  properties: {
    key: 'name',
  },
},

A common pipeline is:

output: {
  postReceive: [
    {
      type: 'rootProperty',
      properties: { property: 'data' },
    },
    {
      type: 'filter',
      properties: {
        pass: '={{$responseItem.active === true}}',
      },
    },
    {
      type: 'setKeyValue',
      properties: {
        name: '={{$responseItem.name}}',
        value: '={{$responseItem.id}}',
      },
    },
    {
      type: 'sort',
      properties: { key: 'name' },
    },
  ],
},

The array is an ordered pipeline. Extract the records before filtering them, create the fields before sorting them, and keep each step small enough to inspect. If you sort by a field that the previous action removed, the result will not match the UI expectation.

Replace the response only when you mean to

The set action replaces the current item with the value from an expression. It can be useful for an operation whose public contract is a small success object:

output: {
  postReceive: [
    {
      type: 'set',
      properties: {
        value: '={{ { "success": true, "response": $response } }}',
      },
    },
  ],
},

Use this for a deliberate node contract, such as a delete operation that has no useful response body. Keep the original response when users need provider fields for later nodes. A replacement that discards the request ID, status, or error context can make support harder.

For a toggle such as "Simplify output," conditionally enable the actions from a node parameter. The current n8n node source uses enabled for this pattern. The setting must be tested in both states, because the simplified and raw outputs are different contracts for downstream expressions.

Use a custom function when the built-in actions fall short

postReceive also accepts an async function instead of an action object. It receives the current items and the full response, and returns the new items:

async function flattenContact(
  this: IExecuteSingleFunctions,
  items: INodeExecutionData[],
  response: IN8nHttpFullResponse,
): Promise<INodeExecutionData[]> {
  return items.map((item) => ({
    json: {
      id: item.json.id,
      email: (item.json.attributes as IDataObject)?.email,
      createdAt: item.json.created_at,
    },
  }));
}

// in the operation routing
output: { postReceive: [flattenContact] },

Keep these functions small and pure. If a function starts calling other endpoints, tracking state, or deciding what to request next, the operation has outgrown declarative routing.

Keep operation output rules local

Different operations from one API often return different envelopes. Put a rootProperty or set action on the operation that needs it rather than applying one global transformation to every response:

Operation Response shape Output rule
Get one One object under record Extract record or keep the envelope when metadata is useful
Get many Array under data Extract data, then filter or sort if the operation promises it
Create Object plus request metadata Keep the ID and status; do not apply the list transformation
Delete Empty body or status response Return a small success object only after the API contract is confirmed

An operation-specific rule also keeps future API changes contained. If a provider changes the list envelope from data to items, the list route and its test change together. If you change the shape your node returns, treat it as a breaking change for workflows that reference those fields, and use node versioning so existing workflows keep the old output.

Know when postReceive is the wrong tool

n8n's node-building style guidance says declarative routing can transform an API response with routing.output.postReceive. It also says to use the programmatic style when the node needs to work on incoming items independently of a single API call or needs transformation beyond what routing handles.

Use a programmatic node when the response logic needs state across calls, joins several responses, performs a complex calculation, depends on external code, or must make a decision from the incoming workflow items before it calls the API. In a programmatic node you shape the response in code inside execute and return INodeExecutionData[] yourself, keeping pairedItem on every output. This post keeps the declarative response pipeline small and visible.

Do not use postReceive as a substitute for request validation or rate-limit policy. Validate parameters before the request, keep retry decisions in the request error path, and test non-success responses separately. A response transformation should receive a response that the node can safely interpret.

Test the final item shape

Inspect the output after all postReceive actions, not only the raw HTTP body. Use a table like this before publication:

Test What to inspect
Wrapped list rootProperty returns one item per record
Single object The operation does not unexpectedly emit an empty list
Empty list The node returns an intentional empty result
Missing root property The failure is visible and diagnosable
Filter true and false Records are included or removed by the documented rule
Missing filter field The result matches the node's stated handling
Key-value mapping Every option has a stable name and value
Sort The visible field is sorted after mapping
Raw versus simplified Each toggle produces the documented output contract
Create or delete response Metadata is preserved or replaced deliberately
Provider error The node exposes safe error context instead of a misleading empty result
Downstream expression A following node can reference the returned fields as documented

Record the n8n version, package version, operation, raw response shape with secrets removed, postReceive actions, final item shape, and any output toggle. Use the NativeShip debugging guide to inspect a route when the final item does not match the response contract. The custom-node guide covers the surrounding package structure.

Run these checks with npm run dev from the package root. From n8n 3.0, n8n-node dev starts n8n in a container, so Docker or Podman must be installed, or use n8n-node dev --external-n8n against an instance started with N8N_DEV_RELOAD=true.

Keep each output example labeled as a pattern until it has been run against the target API and supported n8n version. The final item shape is part of the node's public interface, so a response mapping change deserves the same review as a request-path change.

Frequently asked questions

It applies response transformations after a declarative node receives an API response. Built-in actions can extract a root property (rootProperty), filter items (filter), cap results (limit), map key-value fields (setKeyValue), sort records (sort), or replace an item (set). You can also pass a custom async function.

Use rootProperty when the useful records are nested under a response property such as data, items, or results. Confirm the provider's actual envelope and test a single-object response too.

Add a filter postReceive action with a pass expression that evaluates against each response item. Test true, false, missing, and unexpected field values.

Extract the option array, then use setKeyValue to map the provider's label and stable identifier into name and value fields. Keep the full response for normal record operations when users need more fields.

Sort after setKeyValue when the visible label is the field users should see. Otherwise sort by a field that still exists in the current response item.

No. Use a programmatic node when the logic needs state across calls, joins multiple responses, depends on incoming items, or exceeds the declarative actions that the supported n8n version provides.

Run wrapped, single, empty, malformed, filtered, mapped, sorted, raw, simplified, create, delete, and provider-error cases, then inspect the final items and downstream expressions.

Ready to launch your n8n, Make, Zapier or Activepieces integration?

Give your customers another way to use your product.

Get started