Description
@empirio-ai/n8n-nodes-empirio-ai
n8n community nodes for empirio.ai — build, publish and analyse online
surveys, and start a workflow the moment a respondent submits an answer.
The package contains two nodes:
| Node | Type | What it does |
| ———————- | ——- | ————————————————————————————————————————————————————————- |
| empirio.ai | Action | Create, update, publish, duplicate and delete surveys; add, move, change and remove questions, answer options, logic rules and translations; read full draft/published definitions, responses, aggregates, non-matrix cross-tabulations, statistics and exports |
| empirio.ai Trigger | Trigger | Runs the workflow whenever a response is submitted, delivered over a signed webhook |
Installation · Credentials · empirio.ai node ·
empirio.ai Trigger · Use as an AI tool ·
Self-hosted and Cloud · Rate limits ·
Compatibility
Installation
n8n Cloud
Open the nodes panel on the canvas, search for empirio.ai under More from the community, select
the node and click Install. Only instance owners and admins can install verified community nodes.
Self-hosted n8n
Open Settings → Community nodes → Install a community node, enter @empirio-ai/n8n-nodes-empirio-ai and
confirm.
To install manually instead, run this in your n8n data folder (~/.n8n by default):
npm install @empirio-ai/n8n-nodes-empirio-ai
Restart n8n afterwards.
Requirements
- An empirio.ai account on any plan, including Free. The platform API and webhooks are not
- n8n 2.x. Development and the test suite run against 2.35.7.
- Flat Answers (default) — one item per response with the answers keyed by question title,
- Full Payload — the complete delivery, including the per-cell
answersarray that mirrors the - Pin Delete Mode and Confirm Delete All to literal values in the node. Leaving **Delete
- If the model must choose what to delete, let it fill Response IDs, not Row Numbers. An ID
- Leave On Error at its default on a node attached as a tool. The default reports a refusal to
- Pin the Survey field. This is the one control that bounds the damage — see below — so an agent
- If an agent needs no destructive power at all, do not attach a node with
Deleteselected. The - The endpoint must be HTTPS. Plain
http://URLs are rejected. - Endpoints resolving to private or loopback addresses are rejected, so
localhostand
gated on a plan. Some individual survey features still are, refusing with
planfeaturesexceeded and naming the plan each one needs, and the two export
operations need a paid plan of their own (responses from Lite, charts from Plus),
refusing with planfeaturerequired.
Credentials
Both nodes offer API key, automatic OAuth2 and own-client OAuth2 credentials. Pick
whichever suits the deployment — an API key is the fastest to set up, OAuth2 avoids storing a
long-lived key.
empirio.ai API (API key)
1. Sign in at empirio.ai and open Integrations.
2. Under REST API, create a key. The key is shown once — copy it right away.
3. In n8n, create an empirio.ai API credential and paste the key into API Key.
OAuth2 (Automatic)
For OAuth2 (Automatic), create an empirio.ai OAuth2 credential and click Connect my account.
n8n registers a public client through Dynamic Client Registration and uses PKCE S256.
Approve surveys, responses and webhooks together. This connection serves both nodes and refreshes
access tokens automatically. It works on n8n versions with OAuth2 Dynamic Client Registration,
including Cloud and self-hosted instances. Client ID and secret entry is unnecessary.
OAuth2 (Own Client)
For OAuth2 (Own Client), register a confidential client with the redirect URL of your n8n host.
1. In n8n, create an empirio.ai OAuth2 API credential. n8n shows an OAuth Redirect URL at the
top of the credential — copy it. It looks like
https://.
2. Sign in at empirio.ai, open Integrations and go to **OAuth
clients**.
3. Create a client and paste the copied redirect URL into Redirect URIs. empirio.ai shows the
Client ID and Client secret once — copy both.
4. Back in n8n, paste them into Client ID and Client Secret, then click **Connect my
account** and approve the consent screen.
The credential uses the authorization code flow with PKCE (S256) and requests the surveys,
responses and webhooks scopes. Access tokens are refreshed automatically.
webhooks is required by the trigger node, which registers a delivery endpoint on the survey when
a workflow is activated and removes it on deactivation. If trigger activation reports an
insufficient scope, reconnect the credential and approve the scopes shown above.
> The redirect URL must match exactly, including the scheme and any port. If your n8n host name
> changes, register the new redirect URL with the OAuth client as well.
Testing a credential
All credential types have a built-in test that calls GET /me. Click Save and n8n reports
whether the connection works. It reports nothing else — n8n shows the outcome of a credential test
as a pass/fail message and does not surface the response body, so the plan and the granted scopes
that /me returns are not displayed. A passing test therefore confirms the credential and the host,
not the scopes — a grant missing webhooks still passes here and fails when the trigger registers.
Neither credential has a host or environment field: baseUrl is a hidden, constant property fixed
at https://platform.empirio.ai/rest/v1.
empirio.ai node
Choose a Resource, then an Operation. Surveys are picked from a searchable dropdown, or
addressed by survey URL or ID.
Survey
| Operation | Description |
| ———— | —————————————————————————————————————————————————————————————————————————– |
| Create | Create a draft, either from a prompt (AI mode) or from explicit questions (Manual mode) |
| Delete | Delete a survey and its responses; takes effect immediately and cannot be undone |
| Duplicate | Copy a survey into a new unpublished draft |
| Get | Read the complete working draft and latest published definition, including plan_gate.status (clear, blocked, or unresolved) and blocking feature details |
| Get Many | List surveys owned by or shared with the account |
| Get Stats | View count, started and completed responses, completion rate and completion time |
| Publish | Publish the working draft and return the public survey URL; a blocked owner-plan gate returns planfeaturesexceeded |
| Revert Draft | Throw the working draft away and put the published version back in it. Every unpublished change goes, and nothing you can call brings it back. See Use as an AI tool before attaching this to an agent. |
| Unpublish | Take a survey offline and move it back to draft |
| Update | Apply changes to the working draft without publishing |
Survey → Update offers Input → Builder / JSON in Manual mode. Builder is the default. It has
four repeatable groups: Questions (add, update, delete, reorder), Answer Options
(add, rename, delete, reorder), Logic Rules (add, update, delete), and Translations (survey text,
question text, answer labels). Each row shows the fields for its selected change. Adding questions
reuses Create’s type-specific fields. Multiple groups and general settings become one PATCH.
Question and answer reordering takes the complete ordered list of current IDs. Option changes and
label translations support answer options, matrix rows and matrix columns. Dropdowns read the
working draft and resolve the question and list from the current row. Translation rows can cover
multiple questions and languages; conflicting values for the same translation are rejected.
An update writes only the fields explicitly added to its collection. false is a value, and
Remove Subtitle explicitly clears and hides the subtitle. JSON mode accepts the four API sections
for advanced edits. Hidden fields from the other input mode are ignored, so a workflow that sends its
changes as JSON has to set Input to JSON.
Answer options
Read answer options through Survey → Get, inside each question in draft.questions.
Survey → Update adds, renames, removes and reorders them. Rename preserves option IDs and
attached responses, rules and translations. The node has only the Survey and Response resources;
dropdowns inside the Update builder resolve option IDs from the selected question.
The language dropdown lists the survey’s additional languages. Translate the survey text, questions
and answer labels to provide a complete language. Inspect warnings: text for an unconfigured
language is ignored by the API. The result retains applied_changes and warnings.
Get returns the REST payload unchanged. draft is the complete working definition and
published is the latest published definition or null before the first publish. Analytics
question dropdowns read published.questions, so draft-only questions are never offered for
response filters. Translation entries have to be read together with their status: pending
content may be master-locale text copied in as an editable placeholder, not a completed
target-locale translation. Get Stats remains on the Survey resource because it describes the survey as a
whole even though its REST endpoint sits under /responses/.
Create and Update both accept an AI mode, where empirio.ai generates the change from a prompt,
and a Manual mode with Builder and JSON inputs. JSON accepts question definitions on Create and
question, option, logic-rule operations and translations on Update. General settings remain field
collections in both input modes. Logic Rule Operations accepts add, update and delete entries;
the logic_rules list returned by Get is the stored state, not an edit script.
All twenty-one question types are available, the two image ones included. An option of an
Image Single Choice or Image Multiple Choice question carries an Image URL: give an
https address and empirio.ai fetches the picture, checks it is a PNG, JPEG or WebP within 5 MB,
6000 px per side and 16 megapixels, and stores it. The storage path Get reports is accepted
there too, which is what lets a survey be read and written back unchanged, and a URL that cannot
be fetched refuses the whole call rather than creating a picture round with a gap in it. The same
field sets the survey logo, under Additional Fields → Logo URL on Create.
Both Create and Update carry Run Start and Run End — the survey’s run window. It
activates and deactivates itself at those instants without anybody publishing again, and unlike
every other field it takes effect immediately. Leave one empty to keep what is stored; send the
literal null to remove it.
Create reaches every settings key the API takes — Additional Locales, Hide Branding,
Master Locale and Multi Language Enabled alongside the three toggles.
Logic Rules and Translations appear only when Questions is set to JSON. Both name the
questions and the answer options of the same request by the ref given alongside them, and the
Questions builder offers no refs — a form built for people who do not write JSON should not ask
them to invent identifiers.
With the builder, logic and translations are a second node: Create reports every identifier it
assigned under created — each question and each answer option — and Update takes those ids
directly. In JSON mode the whole survey, its logic and a second language fit in one node, and the
name is stored: Get returns it, and every later call may write it wherever a question_id or an
option_id is expected, including an option added by that same Update. Update additionally takes Logo Path, the storage asset path Get returns
under design — the literal null removes the logo. There is no logo on Create: the path
has to contain the survey ID, which the create is what mints.
Every one of those sections has to carry something. An empty [] or {} is the right shape and
still asks for no change, and the API refuses an update that requests nothing — so the node refuses
it first, naming the field you filled in. **Leave a section out entirely to mean “do not touch this
one.”** The two are not the same thing and the node keeps them apart: a field you never filled in is
omitted in silence, while an expression that resolved to no entries fails the item, because that is
usually a workflow that meant to send something and found nothing. Gate the update on an If node
when a section may legitimately turn out empty.
Revert Draft is the way back out of an Update that went wrong, and it is not an undo of the last
one: it replaces the whole working draft with whatever is currently published, so every
unpublished change is discarded at once, and no call you can make brings it back. What it
leaves alone is the published survey — same public link, same responses, still live — which is also
why a revert leaves no visible mark: nothing on any read surface shows that the work existed. The
item the operation emits is the only account of it you get, under discarded_changes: which sections
differed, the question count before and after, and the wording of every question the draft had
added, removed or changed. The two counts are snapshot sizes rather than a change indicator, so
equal counts can still accompany additions, removals or modifications. Keep that item if the
questions may have to be re-entered.
empirio.ai does keep a short revision history of working drafts on the server side, but it is not an
undo you can build a workflow on: it is unreachable from the API, and it captures at most one
revision per survey every 30 seconds, so an Update followed straight by a Revert Draft usually
records nothing. It is something support may be able to look at after the fact, and nothing more.
Two answers are worth knowing before you build on it. A survey that has never been published has
nothing to fall back on and is refused with a 400 naming both ways on — publish the draft first, or
delete the survey; the draft is not silently emptied. And a draft that already matches the published
version is a success, not a failure: the item comes back with discarded: false and an empty
summary, so a workflow that reverts unconditionally does not need to branch on whether there was
anything to revert.
Response
| Operation | Description |
| —————- | ——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— |
| Delete | Delete responses named by response_id, by row number, or every response of a survey once Confirm Delete All is on. The responses leave every read surface immediately and no API call brings them back. See Use as an AI tool before attaching this to an agent. |
| Export Charts | Render the results as charts: PowerPoint, PowerPoint with raster slides, PDF, DOCX, ZIP of images, or chart JSON. Every format answers with the finished file in one call — the four image-based ones are rendered in the background and the request waits for them. Chart JSON keeps complete configured option and bounded-scale axes, including unused values with count: 0. |
| Export Responses | Export the raw participation data as CSV, XLSX, JSON or SPSS. |
| Get Aggregates | Sparse per-question answer buckets that omit zero-count values, with respondent-based percentages and row/column counts for matrix questions |
| Get Crosstab | Cross-tabulate the answers of two questions |
| Get Many | List participations with their answers |
Both exports take Answer Filters, which narrows the export to the responses that answered
particular questions particular ways — the same filters the results view offers, and the only way
to export a segment. Both operations expose their file type through Format, matching the REST
API, MCP and Make field name format.
Get Many pages through the API automatically when Return All is on. Its Since option takes
an ISO timestamp and returns only responses submitted after it, which makes a scheduled polling
workflow straightforward when a webhook is not an option.
Two of its options cannot be combined: Order set to Ascending together with **Include
Incomplete Participations**. The node refuses that pair itself and sends no request at all, so the
message names the two fields you filled in rather than arriving as an API error. The reason is not
sorting — Descending with Include Incomplete Participations works fine: an ascending page is a
descending window mirrored against the total number of responses, and that total counts only
complete participations, so including the incomplete ones would make the mirrored window slide off
the end and drop the oldest rows. Leave the order at its descending default, or exclude incomplete
participations; row_no is the newest-first rank under either order, so a descending page addresses
exactly the same responses.
Delete offers three ways to say which responses to remove, and the choice matters. **Response
IDs** is where a new node starts. It names the responses by the response_id a read surface
returned — the trigger emits one on every delivery, and every row of Get Many carries one — and an
ID means the same participation under every filter and at every moment. Selected Rows names them
by rowno instead, and is a deliberate switch away from that: a rowno is a rank over the
responses that exist at the moment of the delete, not a handle on a response, so a participation
submitted between your Get Many and your delete shifts every rank by one and row_numbers: [1]
then removes the arrival you never saw. Two deletes in one workflow both saying “row 1” remove two
different responses for the same reason.
A rank is also a rank inside a filtered set, which is the half that gets data destroyed. Get Many narrows its list with Date From, Date To and Include Incomplete Participations,
and row_no counts within whatever those leave — so “row 1” of a filtered list and “row 1” of the
survey can be two different participations. Selected Rows therefore offers the same three under
Filters, and they have to be set to the same values the Get Many that produced the numbers
used. Set them on one side only and the delete still answers 200 deleted_count: 1 while removing a
response that was never in your list. Nothing in the node can catch that: from inside it, a filtered
Get Many feeding an unfiltered delete looks exactly like an unfiltered one feeding an unfiltered
delete, which is correct. That is why the mode is an opt-in rather than a warning. Prefer
Response IDs unless all three filters are deliberately repeated.
All three can change what a rank names, and none of them is the safe one to leave off:
| filter | what it does to the ranks | a mismatch |
| ————————————- | ——————————————————————————————————————————————- | ——————————————————– |
| Date To | trims the newest end, so every row moves up | picks a different participation, reported as success |
| Date From | trims the oldest end; incomplete participations rank after every response, so shortening the response section moves each of them up | picks a different participation, reported as success |
| Include Incomplete Participations | appends the started-but-unsubmitted participations after every response, adding ranks that a delete resolves to submitted responses instead | picks a different participation, reported as success |
| Order | nothing — row_no is newest-first either way, and there is no Order filter on the delete | harmless |
Date From looks like the exception and is not, which is worth one worked example. On a survey of
six responses and two incomplete participations, with Include Incomplete Participations on,
adding a Date From moved the two incomplete rows from rows 7 and 8 to rows 3 and 4 — the
responses kept their numbers, the appended rows did not. A delete that repeated the **Include
Incomplete Participations and left the Date From** off then answered
200 {"deletedcount": 1, "unresolvedrow_numbers": []}, destroying the response that was row 3 of
the unfiltered list — one the workflow had never listed — while the row it had actually selected
survived. Repeat all three, or use Response IDs.
Incomplete participations are also the one kind of row a row_no cannot address: a rank always
resolves to a submitted response, so a part_… row you want gone has to be named in **Response
IDs** (400 by rank, 200 by ID). That is a second reason not to read a rank off a list you
produced with Include Incomplete Participations on.
Nothing checks that for you. The two selectors live on two nodes, and no n8n node can read another
node’s parameters, so the node states the requirement in the editor and cannot enforce it. That is
the practical reason to prefer Response IDs whenever the numbers come from a previous node:
Get Many returns a responseid on the same row as the rowno, and an ID means the same
participation under every filter and at every moment.
Response IDs has neither problem, and it is also the only selector that is safe to retry. An ID
that names nothing in this survey comes back under unresolvedresponseids and the rest are still
deleted; an ID whose response was already deleted comes back under alreadydeletedresponse_ids,
and when that is true of every ID you named the call answers deleted_count: 0 — so re-running a
delete that already succeeded reports what happened instead of deleting something else. Both fields arrive on the item next to deleted_count. Use row numbers
only for a one-off clean-up you are watching; use IDs for anything a workflow runs on its own.
All Responses takes no filters at all. It has no selection to narrow, and the API refuses every
narrowing field on that mode by name — including Include Incomplete Participations switched
off, because n8n sends a filter you added even at its own default. So the Filters control is
shown for Selected Rows only; switch to All Responses and it disappears, and a value left
behind in it from an earlier edit is not sent.
Get Aggregates likewise reports unresolvedquestionids next to its counts — question IDs that
match nothing in the published survey. Check it before reading an absent question as “no answers”:
a mistyped ID, or one that exists only in the working draft, lands there rather than in the results.
Its buckets are sparse and omit configured values with no responses. Percentages use respondents
who answered the question as their denominator, so a multi-select question can sum above 100%; its
totalSelections field reports the number of selections instead.
The question dropdowns of Get Aggregates and Get Crosstab are filled from the survey you
selected, so you never have to look up a question ID by hand. Questions that cannot answer the
operation are left out: display-only content blocks everywhere, because they hold no answers, and
matrix questions in Get Crosstab, which cannot cross-tabulate them.
Get Aggregates reports each bucket by its label, not by an option ID. Two values escape that
resolution and arrive raw: a free-text “other” answer, which is the text the respondent typed, and
an option deleted after the response was submitted, which has no label left and stays an opt_…
id. Current option IDs and labels come from Survey → Get under published.questions; an ID absent
there identifies historical data whose current published definition no longer has that option.
Chart exports answer with the finished file in one call, in every format. The four image-based
formats — DOCX, PDF, PowerPoint (Images) and ZIP of Images — render in a background
job on the API side, but the request waits for it, so the node sees the same shape as for
PowerPoint (Editable) and Chart JSON. A render takes seconds on a typical survey and about a
minute on the largest ones, well inside n8n’s 300-second request timeout.
Chart JSON intentionally uses a denser representation than Get Aggregates for question types with
configured options or a bounded scale: the complete configured axis is present, including unused
values with count: 0, and historical answered values remain visible after the configured domain
changes. Its totalAnswers counts plotted bucket mentions, which means selections rather than
respondents for multi-select questions.
empirio.ai Trigger
The trigger registers a webhook on the selected survey when the workflow is activated, and removes
it again when the workflow is deactivated. Every delivery is signed with a per-webhook secret, and
the trigger verifies the X-EmpirioAi-Signature HMAC before it emits anything. A delivery with a
missing or wrong signature is answered with HTTP 401 and never starts the workflow. empirio.ai
treats any 2xx as delivered, so answering 401 is what makes it retry and then surface a failed
delivery — a silent 200 would discard the response and leave both sides reporting success.
Event is Response Submitted, the one event empirio.ai delivers.
Output controls the shape of the emitted item:
next to the response metadata (responseid, respondedat, surveyid, surveyversion,
duration_seconds, …). This is what most workflows want: the keys read like the survey, so you
can map them straight into a spreadsheet row, a CRM record or a database column set. An
allow_other question contributes a second key, . Two questions sharing a title,
or a title that collides with one of the metadata keys, are disambiguated with a (2), (3) …
suffix, and the companion key follows the key its question actually got — the second Comments?
arrives as Comments? (2) and Comments? (2) (Other). Three kinds of question have no title to
key by and fall back to their raw questionid, which is where a q… key can still turn up: one
that was deleted from the survey after this response was stored, one whose title is empty, and one
whose title is null.
CSV export layout (one entry per scalar answer, multiple-choice option, matrix row, rank position
and free-text companion). Every entry carries questionid, questiontitle and question_type —
so a Switch node can branch on the type without looking anything up, and a report can print the
title without a second call. Beyond those three an entry carries only the identifiers its own kind
has. A multiple-choice entry has optionid next to optionlabel, a matrix row has row_id and
selectedcolumnid, a single-select or rank entry has selectedoptionid — and a text, number
or date entry has none of them: it is questionid + value, with selectedoption_id
absent rather than null. Entries are told apart by which of those identifier fields is present,
not by a discriminator field.
Pick by what consumes the item. Flat Answers is readable but keyed on text a survey editor can
change: rename a question and its key changes with it, and a workflow mapped to the old key stops
finding its value. Full Payload keys on identifiers that a rename does not touch, which is what
you want once something downstream depends on the mapping staying put — with the caveat that a
matrix or ranking entry reports the identifier as it was stored, so it can still name an option the
survey no longer defines.
> A key that disappears fails silently. n8n’s expression evaluator answers a missing key with
> null instead of raising — so a node reading {{ $json['Old title'] }} after a rename stays
> green and writes an empty value downstream. It does not error, it does not warn, and typing
> the field as a number or boolean does not catch it either. Nothing but the data will tell you.
> If that matters, use Full Payload or add an If node that asserts the key is present.
Options → Include Unanswered Questions (Flat Answers only) emits null for questions the
respondent skipped or logic hid. Without it an item only carries the questions that were actually
answered, so the key set differs from response to response and the columns of a downstream
spreadsheet or database node shift. Turn it on when something downstream expects a fixed shape.
Display-only content blocks stay absent either way — there is no answer to report a null for.
Options → Label sets the name shown for the webhook in the empirio.ai dashboard. It defaults to the
workflow name. It applies when the trigger registers a new webhook; a registration the trigger
adopts (see below) keeps the name and owner it already had.
Question IDs are stable for the lifetime of a survey. Inserting, removing or reordering questions
never shifts an existing ID, so a mapping built on Full Payload keeps working, and the same ID
also identifies the question in the empirio.ai node’s analytics operations.
Recovering a lost registration
If a workflow is imported, duplicated or restored, n8n may no longer hold the webhook state. On the
next activation the trigger looks up the survey’s webhooks, matches its own callback URL and adopts
the existing registration. Because empirio.ai hands out a webhook’s signing secret only once, the
trigger rotates the secret in that case so it can verify deliveries again. Rotating affects only
this registration — other webhooks on the survey are untouched.
Use as an AI tool
The empirio.ai node is usableAsTool, so it can be attached to an AI Agent or exposed through an
MCP Server Trigger. Read this before you attach one that can write.
What the model can drive. n8n publishes a tool schema built from the node’s parameters. It builds
it by walking the stored parameter values and collecting every $fromAI(...) call it finds, whatever
the field is — so each parameter the workflow author filled in with such an expression becomes an
input the model supplies. Anything the author typed in as a literal is fixed and the model cannot
change it. A tools/call supplying confirmDeleteAll: true performs a real delete of every response
in the survey; one supplying deleteMode: "ids" and a list of responseIds deletes exactly the
participations it names.
Response → Delete has nine such inputs, not the four or five you might count off the fields
in front of you: deleteMode, responseIds, rowNumbers, confirmDeleteAll, the three Filters
entries dateFrom / dateTo / includeIncompleteParticipations, idempotencyKey, and the
Survey field. Nine is the ceiling, and reaching it takes a node whose Delete Mode is itself
model-filled. The walk runs over the parameters as the mode displays them, so pinning Delete Mode
to a literal removes the other modes’ fields from the published schema — measured on five nodes of
one MCP Server Trigger whose stored parameters were identical except for that one value:
| stored Delete Mode | published tool inputs |
| —————————————- | ——————————————————————————————————- |
| $fromAI(…) | 9 — every field above |
| "ids" | 3 — surveyId, responseIds, idempotencyKey |
| "rows" | 6 — surveyId, rowNumbers, dateFrom, dateTo, includeIncompleteParticipations, idempotencyKey |
| "all" | 3 — surveyId, confirmDeleteAll, idempotencyKey |
| not set — the node left on its default | 3 — the "ids" row: surveyId, responseIds, idempotencyKey |
Pinning the mode is the first item in the list below for a second reason, then: it is the one edit
that shrinks what the model is offered at all. Note what it does not do. confirmDeleteAll is
displayed by deleteMode: "all", so pinning the mode to "all" keeps that field in the schema —
as a required input, measured driving {"confirmDeleteAll": true} to deleted_count: 8 on a
mode-pinned tool. Pinning the mode narrows which fields a model can reach; only pinning
Confirm Delete All itself takes the delete-everything switch out of its hands, which is why the
list below names both.
Confirm Delete All and Delete Mode carry noDataExpression, which removes the expression editor
and the editor’s “let the model define this parameter” button from those two fields. Six visible fields of
this operation do not carry it — Survey, Response IDs, Row Numbers and the three Filters
entries — so the editor offers the button on those six. idempotencyKey is hidden; a stored
$fromAI expression on it still participates in the measured schema above. And on the two that do
carry it, it is a guard-rail in the editor, not an access control: noDataExpression is a
property of the field definition, while the schema is built from the stored values, so a workflow
whose stored parameters already hold $fromAI expressions — hand-written, imported, or built by the
API — publishes them to the model regardless. displayOptions is the only thing that keeps a field
out, and only while the parameter it depends on holds a literal.
So the safe shape for an agent-facing delete is:
Mode** alone gets you the ids schema, which is the right one; pinning it says so explicitly and
survives a future change of default.
names one participation for its whole life, so the model deletes what it named and nothing else,
and a retry repeats that same delete. A row number is a rank resolved when the call runs, inside
whatever the Filters select: a response arriving in between shifts every rank, and a filter the
model chooses differently from the list it read renumbers the whole set — the model’s “row 1” is
then somebody else, reported as a success. Pinning the mode to ids also takes rowNumbers and
all three filters out of the schema, so there is nothing left for the model to get wrong.
the model as isError: true with the API’s own sentence; Continue (using regular output) hands
it [{}], which is indistinguishable from a successful delete. See
Errors reaching a model.
should get a node scoped to one survey rather than one whose survey ID it chooses.
operation is part of the tool the model sees, not something it asks permission for. The same goes
for Survey → Delete and Survey → Revert Draft — see
Survey → Revert Draft.
What the node refuses regardless. Every delete guard runs on every call, whoever made it. A
mode: "all" request is refused unless confirmDeleteAll is a real true; a value that merely
looks set — "false", "no", "0", a stringified expression — is refused by name rather than read
for truthiness. A Delete Mode that is none of ids, rows and all is refused rather than
falling through all three guards. Each guard is a no-op outside its own mode, so row numbers left in
the node are not sent with an ids delete and vice versa. Every value in Response IDs must look
like a responseid — a resp or part_ prefix and 22 more characters — so a row number, a survey
UUID or a truncated ID fails in the node, by name, before anything is sent; an empty list and a list
longer than 1000 are refused the same way. The same discipline applies to every other guarded
parameter in the node.
What that does and does not bound. With Response IDs model-fillable, three things limit the
damage. The shape check above means the model cannot turn a number it saw somewhere into a delete.
The API resolves IDs within the selected survey only: an ID belonging to a different survey is
reported back under unresolvedresponseids and deletes nothing there, and a call in which no ID
names a participation of this survey at all — live or already deleted — is refused outright with
“No participation of this survey matches” — so a pinned Survey field is a hard ceiling on what
any tool call can reach. And an ID is a 128-bit digest of a random
internal key, so it cannot be guessed or enumerated: the only IDs a model has are the ones a read
surface handed it.
What none of that bounds is the survey itself. There is no confirmation toggle on ids the way
confirmDeleteAll gates all, and an agent that has just run Response → Get Many is holding every
response_id in that survey. Within one survey, a model that can call this tool can delete any
participation it chooses, in batches of up to 1000, with no undo. ids is the safer selector —
it deletes only what it names and it is safe to retry — but it is not the weaker one. Treat the
survey pin, and the decision to attach Delete at all, as the access control; the guards above are
only there to stop a malformed call.
Survey → Revert Draft, and the other whole-survey writes
Response → Delete is the operation with the most model-reachable surface. It is not the only one
that destroys work, and the operations that do are the ones with the smallest surface — which is
why they are easy to miss on a node built for something else.
| operation | published tool inputs | what a call costs |
| ————————- | ——————————– | ————————————————————————— |
| Survey → Revert Draft | 2 — surveyId, idempotencyKey | every unpublished change in the draft, unrecoverable; live survey untouched |
| Survey → Delete | 2 — surveyId, idempotencyKey | the survey and all its responses |
| Survey → Publish | 2 — surveyId, idempotencyKey | pushes the current draft live |
| Survey → Unpublish | 2 — surveyId, idempotencyKey | takes the survey offline |
Two is the floor for a write in this node, and for Revert Draft it is also the ceiling: the
operation has no selector, no filters and no confirmation toggle, because there is nothing to select
— it always takes the whole draft of the one survey the node addresses. So on a node whose
Operation holds a literal, the only thing a model chooses is which survey, and **pinning the
Survey field is the whole of the remaining access control**, exactly as it is for Delete. An agent
holding an unpinned Revert Draft can empty the draft of every survey in the account, one call each,
and every one of those calls answers 200.
“Whose Operation holds a literal” is a real condition, not a formality. Every number in the table
is measured with Resource and Operation pinned, which is what a node built in the editor
stores — and the schema is built from stored values, the same door the paragraph above the delete
list describes. A node whose stored Operation holds a $fromAI expression publishes operation
as a model input as well, next to surveyId and idempotencyKey. Leaving selectors unpinned exposes
the fields of the operations they can select, including the survey builder. On such a node a model
given the tool for Survey → Get
can select revertDraft and name the survey in the same call, and the operation the author chose
bounds nothing. Operation and Resource both carry noDataExpression, so the editor never offers
the “let the model define this parameter” button on them — but a workflow imported or built through
the API does not go through the editor. Pin Operation as well as Survey, and prefer attaching
a node whose selected operation is one you would let the model run unattended.
A confirmation toggle would not change that. confirmDeleteAll earns its place on Response → because that operation has a selector: a model-filled
DeleteDelete Mode can escalate a named
list of IDs into “every response in the survey”, and the toggle is the second lock on the escalation.
Revert Draft has no such step to lock, so a toggle would only make every legitimate use refuse until
someone switched it on — and it would not be an access control in any case, for the reason the
paragraph above the delete list already gives: the schema is built from stored values, so
noDataExpression keeps a field out of the editor’s expression menu and not out of the model’s
schema.
Nothing on a read surface shows that a revert happened. The published survey, its public link and its
responses are exactly as they were, and haspendingdraft_changes simply reads false — which is
also what an untouched survey reads. The emitted item’s discarded_changes is the only account you
get, so
leave On Error at its default (see Errors reaching a model) and keep
the item if the questions may have to be re-entered.
Retries. See Idempotency: under an AI Agent, the derived key comes from the
call’s parameters, and under an MCP Server Trigger the node cannot derive a retry-safe key at all.
Revert Draft is idempotent at the API as well — a second call finds the draft already equal to the
published version and answers 200 with discarded: false, having written nothing.
Errors reaching a model
An API refusal is legible on the two paths you get by default, and illegible on the one you have to
choose. The same refusal, three settings:
| setting | what arrives |
| ———————————————— | ———————————————————————————————– |
| default (fail the item) | NodeApiError whose description is the API’s own sentence |
| tool call, default error handling | isError: true, text ending Details: |
| On Error = Continue (using regular output) | workflow item {"error": "Bad request - please check your parameters"}; tool result [{}] |
Both losses are n8n’s, not this node’s, and neither has a hook the node could use. A declarative node
never sees the failure: n8n’s RoutingNode catches it and pushes { json: {}, error } — the
explanation lives in the sibling error, not in json. The workflow engine then rewrites json from
error.message, the generic HTTP summary, dropping error.description, which is where the API’s
sentence is. The tool wrapper never runs that rewrite at all: it maps a result with
items.flatMap(item => item.json) and JSON.stringifys it, so the sibling is discarded and an empty
object is all that is left. It carries no isError, so a model cannot tell it from a successful write.
The one thing to do about it is the setting itself: **leave On Error at its default on any node a
model can call.** If a workflow must continue past a failed delete anyway, note that $json.error
downstream holds only that generic summary — the API’s sentence survives in the run’s error record,
which is what the editor shows on the node, not in anything a later node can branch on.
Self-hosted and Cloud
empirio.ai only delivers to endpoints it can reach:
192.168.x.x do not work.
n8n Cloud satisfies both conditions out of the box — the empirio.ai Trigger works with no extra
setup.
Self-hosted n8n needs a publicly reachable HTTPS host name. Set WEBHOOK_URL to that public URL
so n8n hands the correct address to empirio.ai, and put a TLS-terminating reverse proxy or tunnel in
front of the instance. During local development, n8n started through a tunnel works as well.
The empirio.ai node itself has no such requirement — it makes outbound calls only and works on any n8n
instance with internet access.
Example workflow
A weekly CSV of a survey’s responses. Export Responses answers with a signed download_url
rather than the bytes, so a second step fetches it — that keeps a large export out of the workflow’s
memory and lets the URL be passed to storage or email instead.
Replace YOURSURVEYID with the ID from Survey → Get Many, or pick the survey from the list in
the node. Paste this into a new workflow with Ctrl/Cmd+V:
{
"name": "empirio.ai u00b7 weekly CSV of new responses",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "weeks"
}
]
}
},
"id": "a1",
"name": "Every Monday",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [0, 0]
},
{
"parameters": {
"resource": "response",
"operation": "exportResponses",
"surveyId": {
"__rl": true,
"mode": "list",
"value": "YOURSURVEYID"
},
"format": "csv",
"options": {
"includeIncompleteParticipations": false
}
},
"id": "a2",
"name": "Export responses",
"type": "CUSTOM.empirioAi",
"typeVersion": 1,
"position": [220, 0],
"credentials": {
"empirioAiApi": {
"id": "1",
"name": "empirio.ai account"
}
}
},
{
"parameters": {
"url": "={{ $json.download_url }}",
"options": {
"response": {
"response": {
"responseFormat": "file"
}
}
}
},
"id": "a3",
"name": "Download the file",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [440, 0]
}
],
"connections": {
"Every Monday": {
"main": [
[
{
"node": "Export responses",
"type": "main",
"index": 0
}
]
]
},
"Export responses": {
"main": [
[
{
"node": "Download the file",
"type": "main",
"index": 0
}
]
]
}
}
}
For charts instead of raw data, swap the operation for Export Charts and pick one of its six
formats; everything else stays as it is.
Rate limits and idempotency
The empirio.ai API allows 60 read and 20 write requests per minute per user account, not per
token — the budget is keyed on the account, so two n8n instances, or an n8n workflow and a Zap,
running under the same account share one window. When a limit is hit the API answers 429 with a
Retry-After header.
Retry On Fail cannot recover from a 429. n8n caps the node at 5 tries with at most 5 seconds
between them, and it does not read Retry-After, so the longest backoff it can produce is about
20 seconds against a window that lasts 60. A saturated window outlives every retry, and the node
fails anyway. Space the calls out instead: a Wait node, a smaller batch size, or a schedule that
does not fire every workflow at once. Retry On Fail is still worth enabling for transient network
and 5xx failures, which it does fix.
Idempotency
Every write automatically carries an Idempotency-Key, and the API applies a repeated key at most
once within 24 hours. The editor contains no idempotency input. Saved idempotencyKey values remain
available to the runtime as hidden parameters.
By default the node derives that key from the execution itself, as
n8n- — the execution ID, the node, the pass over its input and the
item index. Two consequences worth knowing:
Retry On Fail‘s attempts belong to the same execution, node, pass and item, so the second attempt of a failed write is recognised as the same
write and applied once. This matters most for Response → Delete in Selected Rows mode: row
numbers are a rank that shifts the moment a delete lands, so a retry that counted as a new request
would remove different rows.
delivery are new executions with new keys, and each is applied on its own.
#### As an AI tool
n8n runs a node attached to an AI Agent through a different loop than a workflow step, and that
loop advances the run index on every retry attempt rather than fixing it for the write. The pass
index therefore cannot tell “attempt 2 of one call” from “call 2”, so on this path the key is derived
from the call’s own resolved parameters instead, as n8n-. Retries
stay safe, which is the property that matters for the destructive operations. The trade-off: **two
tool calls that ask for byte-identical writes inside one execution count as one write.** If an agent
should be able to create two identical drafts in a single run, give it something to vary.
Under an MCP Server Trigger there is no execution to derive from at all: n8n does not give the
node an execution ID on that host, and there is nothing else that separates one MCP session from the
next. Rather than mint a key that would look stable and in fact be _the same key for unrelated
writes days apart_, the node falls back to a random key there. That means:
> Writes made through an MCP Server Trigger are not retry-protected. Do not enable
> Retry On Fail on Response → Delete in Selected Rows mode on that host — a retried attempt
> is a second, independent delete against re-ranked rows.
A saved key is bound to its request data for 24 hours: reusing it after the data changes fails
with idempotencykeyconflict. A saved expression that resolves to null, undefined or a blank
still fails the item, so it cannot silently lose its retry protection.
Failed authentications
Refused authentications are counted separately from the request budget, against **the credential that
was presented**, at 120 a minute. A workflow whose API key has been revoked or mistyped exhausts that
budget on its own and is answered 429 until the window rolls over; nothing else is affected.
The bucket is the credential rather than the caller’s address, and that is the part that matters on
n8n Cloud: every workflow on the platform leaves from a small pool of shared addresses, so a
budget keyed on the address would let one tenant’s stale key lock out everyone else’s working ones.
Yours is charged to your credential and to nothing else.
Compatibility
Developed and tested against n8n 2.35.7. The package declares engines.node >= 20, which is what
its own code needs; n8n’s minimum is higher (2.x asks for Node 22) and is what actually applies at
runtime.
License
Partial logic-rule updates
In Update Survey, a logic-rule update sends only the properties selected under What to Change.
Changing just the action preserves the rule’s question, condition, compared values and identity.
End the Survey removes the target reference. A targeted action requires a target question.
Compared-value dropdowns use the rule’s current question unless a new question is explicitly selected.
The forms do not copy an editor-time snapshot into the update.
n8n and Zapier allow several properties to be selected together. Make selects a property group per
row; Question, condition and values updates a complete comparison together. Other properties
remain unchanged. An incompatible comparison is rejected by the API.
The n8n examples update-survey-builder.json and update-survey-edge-cases.json in
integrations/n8n/examples/ create fresh draft surveys and check persisted results through Get
steps and assertions. They cover question and option CRUD, reordering, matrix columns,
translations, partial rule edits, invalid orders, atomic rejection and idempotency. Requests are
paced to respect the API budget; run the workflows sequentially. They leave drafts for inspection
and never publish them. The examples use the local development node type CUSTOM.empirioAi;
select a credential pointing to the local API before running them.