Package Information
Available Nodes
Documentation generating…
Documentation
@twovmodules/n8n-nodes-sourcetodocs
The SourceToDocs node for n8n does document
generation: it generates Google Slides and Google Docs documents (with an optional PDF alongside)
from the rows flowing through your workflow — a weekly report from your CRM rows, or one
invoice PDF per customer through a single split Document, in the way of a mail merge. It also reads your projects, documents and runs, so a workflow can pick
a Document by name, see its Data Sets' column names before sending a row, and poll a run's
outcome without using Generate's own wait.
n8n is a fair-code licensed workflow
automation platform.
- Installation
- Operations
- Credentials
- Compatibility
- Usage
- Resources
- Version history
- Development
- License
Installation
Follow the community nodes installation guide
and install @twovmodules/n8n-nodes-sourcetodocs. On n8n Cloud, look for SourceToDocs in the nodes
panel once the node is verified.
Operations
| Resource | Operation | What it does |
|---|---|---|
| Project | Get Many | List every project in the workspace |
| Document | Get Many | List the documents of a project |
| Document | Get | Get a document: its Data Sets (with column names), its split configuration and its Distributions |
| Document | Generate | Start a run — from the Document's connected sources, or from rows sent in this workflow — and return the produced file's link |
| Run | Get | Get a run's status, its file and its deliveries; a batch also lists its children unless Summary Only is on |
Project → Get Many, Document → Get Many and Document → Get return the API's own objects as
items, unmodified. Run → Get flattens every item exactly as Generate does — an ordinary run,
a Summary Only batch, and each child alike — so the next node reads file_url at the top level
either way. Generate and Run → Get both offer Output (below) to keep or drop the full run
object from each item; Document → Get always returns the API's raw object.
Project → Get Many and Document → Get Many (version 1.1) have Return All (on) and Limit
(50, shown when Return All is off). The API has no paging, so Limit cuts the list the node
already has, in one request. A Limit that is not a whole number of at least 1 is refused.
Version 1 has neither and returns everything.
Credentials
You need a SourceToDocs account with a workspace (app.sourcetodocs.com);
the API Token is created in that workspace, on its API Tokens page. Only a workspace owner or admin can create an API Token: a member asks one of them.
Create a SourceToDocs API credential with:
| Field | Value |
|---|---|
| API Token | A workspace API Token from the API Tokens page in the SourceToDocs app. It starts with wsk_ and is shown once, when it is created. |
| Base URL | https://app.sourcetodocs.com/api/public/v1. Keep the default. |
The token is sent as Authorization: Bearer <token>. It may do everything the Public API
exposes, for its own workspace and nothing else. The credential is restricted to this node (and
its AI tool variant): n8n refuses to save a workflow where another node, an HTTP Request node for
example, uses it.
Base URL must use https. A plain http address is refused before a request is made, with
an error naming Base URL, because every request carries the token. The node also never follows a
redirect (a redirect would send the token on to another address): an answer such as 301 or302 becomes an error that says to set Base URL to the API root.
Test in the credential dialog lists the workspace's projects (GET /projects) and sends
the API Token to the Base URL you entered, so check the address before you press it. It reports
four cases in their own words: a rejected token (401), a Base URL where nothing answers (404),
the rate limit (429), and a 200 that is not the Public API's JSON — Base URL set to the app
itself, for example, which answers with its HTML page. Test does not apply the https rule: that
check runs when the node makes a request.
Compatibility
Built and tested against n8n 2.36.9 (self-hosted), with n8n's own engine in the test suite. It
uses recent n8n features (a credential restricted to one node, @version display rules, resource
mappers, the AI tool variant), so keep n8n current; earlier versions are untested. The node works as a tool for n8n's AI Agent; on a self-hosted n8n, using a
community node as an AI tool may need to be switched on in the instance's settings (see n8n's
documentation on community nodes as tools). It has no runtime dependencies.
Usage
Generate a report from your CRM rows
This is the weekly-report-from-crm-deals.json
template end to end: a CRM's rows become a document, with no mapping step.
Add a node before SourceToDocs that produces one item per row you want in the report — an
Airtable or HubSpot Get Many, a Postgres query, a Set node, anything. Each item's.json
becomes one row.Add the SourceToDocs node, credential attached, Resource: Document,
Operation: Generate.Pick Project, then Document — From List once a Project is picked, or paste the
document's URL from the app (.../documents/show/<id>), or its ID directly.Set Rows Come From to This Workflow — JSON. The Data Sets collection appears, already
holding one entry with Rows pre-filled:{{ $input.all().map(i => i.json) }}Pick a Data Set for that entry — its own dropdown description lists the Data Set's
column names, which is exactly what each row's fields must spell, case included.Leave Unknown Fields on Drop (the default). Most upstream nodes add their own fields to
every item they produce on top of the ones you actually picked — an Airtable read carriesid/createdTime, an n8n Form Trigger submission carriessubmittedAt/formMode— so the
node removes anything that is not one of the Data Set's own columns before sending, and
reports what it removed asdropped_keyson the output item (one array per Data Set, only
where something was actually dropped). This is what makes step 4's plain{{ $input.all().map(i => i.json) }}work straight off an Airtable or Form Trigger node with
no projection step: a genuine typo in a column name you did mean to send still shows up indropped_keys, it just does not fail the run. Switch to Refuse when you want the opposite
trade — every field sent exactly as given, and a stray field becomes the API's own422 inline_column_unknownnaming it, rather than being silently removed. Drop reads the
Document once before the run (one extraGET) to learn each Data Set's columns; Refuse
sends the rows as given and does not read it.Leave Deliver To empty to use the Document's own distributions, or pick a subset.
Leave Wait For Completion on (the default). Max Wait (300 s) and Poll Every
(5 s) control how long and how often the node polls before giving up; the bounds are in
Wait or poll.Run it. The output item carries
file_urlandfile_nameat the top level — hand those
straight to a Slack, email or HTTP node. Output (default Simplified) keeps the
flattened fields; Raw adds the complete run object the API returned, underrun; see
Node versions and the Output option.
Two things about the input. First, with 0 input items the node does not run — n8n skips a
node that receives nothing, so a scheduled report whose upstream node found no rows generates
nothing and says nothing. To run it anyway, turn Always Output Data on in the settings of
the node before SourceToDocs: it then runs once, on one empty item (in JSON mode that item
is sent as one blank row, which the output reports as empty_rows: 1). Second, every
parameter except the Map Columns cells reads the first input item only: a Document, Deliver
To or Idempotency Key written as an expression such as {{ $json.doc }} takes item 0's value
for the whole run, because Generate makes one request however many items arrive. Only the
Map Columns cells are resolved once per item. (The read operations, Project, Document and Run,
do run once per item.)
The node's parameters, in order, for reference:
| Parameter | Type | Default | Notes |
|---|---|---|---|
| Project | options | — | Only filters the Document picker; never sent |
| Document | resource locator | — | From List / ID / URL |
| Rows Come From | options | Connected Data Sources | connected, workflow (This Workflow — JSON) or mapped (This Workflow — Map Columns) |
| Data Sets | collection | one empty entry | shown for workflow only |
| → Data Set | options | — | per entry; its description lists column names |
| → Rows | json | {{ $input.all().map(i => i.json) }} on the first entry |
an array of row objects; a run takes a request body of up to 2 MiB (about 15 000 ordinary rows) and up to the plan's row limit per Data Set, see Limits |
| Data Set (Map Columns) | options | — | shown for mapped only; the one Data Set whose columns are mapped |
| Columns | resource mapper | Map Each Column Manually | shown for mapped only; see below |
| Additional Data Sets | collection | empty | shown for mapped only; JSON rows for other Data Sets |
| Unknown Fields | options | Drop |
shown for workflow and mapped; drop or refuse |
| Deliver To | multi-select | none | the Document's enabled Distributions; one that was deleted or disabled is ignored by the API, the others still get the file |
| Wait For Completion | boolean | true |
|
| Max Wait | number (s) | 300, at least Poll Every, at most 3600 |
shown while waiting |
| Poll Every | number (s) | 5, at least 2, at most 3600 |
shown while waiting |
| Idempotency Key | string | empty | empty: the node sends its own key; see Retry On Fail. An expression may give text or a number (a number is used as its text); one that gives nothing, or anything else, stops the node before it sends anything |
| Output | options | Simplified |
node version 1.1 and later: Simplified, Raw or Selected Fields |
| Fields | multi-select | run_id, status, file_url |
shown for Selected Fields only; run_id (and batch_run_id on a batch's files) is always kept, so a timed-out item can still be polled |
| Simplify | boolean | true |
node version 1 only; off keeps the full run object under run |
Every id the node puts in a URL or in the request — the Document in any of its three modes, the
Project, the Run, a Data Set, a Deliver To entry — must be a UUID. Anything else, an expression
or an AI agent's value included, is refused before any request, with an error that names the
parameter.
Three ways to get rows into Rows, all from the field's own hint:
all input items: {{ $input.all().map(i => i.json) }}
an array field of the items: {{ $input.all().flatMap(i => i.json.Lines) }}
another node's items: {{ $('Get lines').all().map(i => i.json) }}
Map columns instead of writing JSON
The CRM recipe above sends JSON. When each incoming item is one row — a Form submission, an
Airtable record, a webhook call — pick Rows Come From: This Workflow — Map Columns and map
the columns the way you would in Google Sheets or Airtable. Which choice to make:
| Rows Come From | Pick it when |
|---|---|
| Connected Data Sources (default) | The Document reads its own sources and nothing comes from the workflow. |
| This Workflow — Map Columns | One row per incoming item. Pick a Data Set, map a value into each column, no JSON. |
| This Workflow — JSON | Rows come from another node, from an array inside an item, or for several Data Sets at once — the invoice recipe stays JSON. |
With Map Columns:
- Pick the Document, then the Data Set (shown only in this mode). The Columns mapper
fills with that Data Set's Table columns, in the order SourceToDocs lists them. - The columns the Data Set selects in the app are shown at once; every other column of its
Table is in the mapper's "add column" list, because the API accepts any column of the Table.
A Data Set that selects no column shows all of them. A column's type only picks the
input: a date column gets n8n's date picker, a select column whose choices the API
publishes gets a dropdown of them, and every other column — number and true/false included —
is a text field. Values are sent as the mapping resolves them: n8n does not convert or
check them, and the SourceToDocs API decides what a row may hold, as in JSON mode. See
What the mapper sends for the exact result of each kind of value. - Choose how to map, with the mapper's own switch:
- Map Each Column Manually — drag or type a value into each shown column. Exactly those
values are sent: one row per input item, each item's expressions read against that item.
Unknown Fields does not apply to these rows. If no input item carries any mapped value the node
stops before sending anything. The error names the shown columns whose expressions resolved
to nothing and suggests a Split Out node (or.map(...)for a list field) first; picking
another Document or Data Set resets the mapping, so map the columns again (or use Map
Automatically). - Execute Once on the SourceToDocs node: Map Columns then sends only the first item's row
(JSON mode's$input.all()still sees every item). The node adds a hint to the output
panel saying so; for several rows use Rows Come From: This Workflow — JSON. - Map Automatically — every input item's own fields are sent as its row. Unknown Fields
then works exactly as in JSON mode: Drop (the default) removes fields that are not the
Data Set's columns — an Airtableid/createdTime, a Form'ssubmittedAt— and reports
them asdropped_keys; Refuse sends them and the API's422 inline_column_unknownnames every unknown key (up to 20). - A split Document needs its key. If the Document splits, map the split's key column(s)
(the split's Driver table column, for example the customer's email) — or use Map
Automatically or JSON. A row with no key produces no file: the run succeeds withfile_count0 and no error. Where the API can name the key columns (see below) the node
reads the Document and, when every item leaves a key column empty, still sends the run but
addsmissing_split_keys: [<column>, …]to the output item and a hint.
- Map Each Column Manually — drag or type a value into each shown column. Exactly those
- Rows for other Data Sets go in Additional Data Sets, the same collection as Data Sets
in JSON mode (Rows is a JSON array, often{{ $('Other node').all().map(i => i.json) }}). The
Data Set you mapped may not also be listed there: the node refuses that before sending anything.
The request is still one Generate per execution, however many items arrive. As a tool for an AI
Agent, each mapped column can be handed to the model with n8n's "let the model define this
parameter" button, so an Agent fills the columns one by one instead of composing a JSON array
(read Use it as an AI tool first).
What the mapper sends
A mapped value is sent as it resolved. Measured on n8n's engine, on a text, number and date column:
| You map | What goes in the row |
|---|---|
{{ $json.str }} where it is the text 12 |
"12", not the number |
the number 0, the boolean false |
0, false |
a resolved null |
null |
{{ $json.empty }} where it is '', {{ '' }}, a bare =, or an empty literal in a number column |
"" (the API renders it as an empty cell) |
{{ $json.missing }}, a key the item does not have |
the key is left out of the row |
{{ }} |
not sent: n8n raises "invalid syntax" |
a date such as 2026-03-01, or {{ $now }} |
the text, or an ISO 8601 string carrying the instance's UTC offset |
So only a value that really resolved to null goes as null; "no value" is usually "" or a
missing key. The one literal n8n itself refuses before the run is a wrong-typed value in a
field that has a type: an empty literal in a date column ("expects a dateTime") or, for a column
a node saved before the number and true/false fields became text, text in a number column. That
stops the workflow with a workflow-issue error and sends nothing; an expression is never checked.
Columns, rows and keys the node reports
The run is sent in each of these cases; the output item says what was missing.
empty_columns— a column the mapper shows (one you have not removed from the mapper)
whose value isundefinedornullin every input item, typically an expression that read
a list field ({{ $json.clients.name }}) or a field the items do not have. A column that is
empty in only some items is normal and is not listed. The node also adds a hint suggesting a
Split Out node before it, or.map(...)on the list. Map the column, or remove it from the
mapper, to silence it.empty_rows: n — rows that are{}in everything sent from this workflow (an item with
only binary data, for example, or a row whose fields all went away under Unknown Fields: Drop,
because none matched a column), plus a hint. This covers Map Automatically, Additional Data
Sets and JSON mode.missing_split_keys— see the split Document above.
Number and true/false columns used to be seeded by n8n's editor with 0 and false, which
then rendered as a real 0 or "No". Now they are text fields and an untouched one stores nothing.
A Map Columns node you saved before that keeps what it stored, 0 and false included: n8n
does not refresh the stored mapping when you open or save the node. Set those values yourself, or
add a fresh SourceToDocs node.
A select column offers the published choices as a dropdown. The API publishes them for
Airtable single and multiple selects, for a Google Sheets column with a list validation, and for
a CSV column the Data Set types as a select with a list; any other select stays a free-text
field. A literal value that is not in the list stops the workflow before the run (n8n checks it,
for example when a choice was added in the source after the mapper's columns were stored);
refresh the mapper's columns, or map it with an expression, which is sent as it is. Because the
mapper reads the Document, each Map Columns run costs one extra GET of it per execution
(Unknown Fields: Drop makes the same read in JSON mode; one read serves both).
One invoice per customer
This is the one-invoice-per-customer.json template:
one Generate call produces one invoice per customer, each emailed to its own recipient — a
split document used the way it is meant to be used from an integration. An If node,
Has an invoice?, sits before Gmail and lets through only items whose status is succeeded
and that have a file_url. Every other item (a file that failed, was cancelled or finished with warnings, or a batch that
timed out or produced nothing) goes to a No invoice, not emailed node, so you can see which
customers got nothing, and the execution does not fail. The exception is a batch in which every
file failed: the batch is failed, so the node stops the workflow with the failure.
Airtable returns a linked-record field as record ids and a lookup field as an array of scalars,
never as row objects — so a customer row's own Lines field cannot hold line-item objects toflatMap over. The two tables have to come from two separate reads, joined only by a column
your own data carries, not by n8n:
- In the app, configure the Document to split on your Customers table, keyed on the column
you want one file per — an email address works well, because it lets the next node address
each file without a lookup (see step 5). Configure the invoice lines Data Set against your
Invoice Lines table, scoped by a customer key column that table must carry — a Customer
ID or email field that matches one row of Customers — so the split can tell each customer's
lines apart. - Feed the SourceToDocs node from two Airtable (or similar) Get Many nodes: one of your
Customers table ("Get customers"), one of your Invoice Lines table ("Get invoice lines").
Both must have run before SourceToDocs, and n8n's current execution order (v1, the
default for every workflow made in the editor) runs only the nodes connected into the path, so
chain them:Get customers → Get invoice lines → SourceToDocs, with Execute Once onGet invoice lines(it would otherwise read once per customer). The template does exactly this and
setsexecutionOrdertov1. - Rows Come From: This Workflow, with two Data Sets, each referencing its own node by
name rather than$input(the "another node's items" recipe), since SourceToDocs is only
connected to one of the two reads:- the one on your Customers table —
{{ $('Get customers').all().map(i => i.json) }}. This is also the Data Set the split reads
its driver from (its table matches the split's), so its rows decide how many files come out
— one per distinct value of the split's key column. - the one on your Invoice Lines table —
{{ $('Get invoice lines').all().map(i => i.json) }}. Every row needs the customer key
column from step 1; a line with none, or with a value that matches no customer, never
reaches that customer's file. - Leave Unknown Fields on Drop: both Airtable reads carry
id/createdTimeon every
record, and neither table's own columns include them. - Deliver To does not apply to a split Document: the node's own dropdown shows a notice
instead of a choice, because a split run delivers through the Document's own
distributions, not a per-call selection.
- the one on your Customers table —
- Wait For Completion on. The output is one item per customer's file, each already
carryingsplit_key(the driver value that file was generated for — the customer's email,
per step 1) alongsidefile_url. - Feed that straight into a Gmail or Send Email node's To field:
{{ $json.split_key }}—
not an expression on the Customers node, such as{{ $('Get customers').item.json.Email }}.
Every output item of a Generate call pairs to every input item, not the one that happens to
produce that file, so with more than one customer.itemon the Customers node throws
"Multiple matches found" (with exactly one customer it resolves to that customer).split_key
is the one field guaranteed to name the right customer for each item.
A key that does not belong to a Data Set's own table, a Data Set picked twice, or no inline Data
Set sitting on the split's driver table, are each a distinct 422 with a code — see
Errors. If every customer's Lines-equivalent rows end up blank, check the customer
key column spelling and values first — a mismatch there is silent, not a 422.
Get the PDF bytes
SourceToDocs never puts a file's bytes on the wire — Generate returns a link, same as every
read. To get the bytes into your workflow:
- Turn on the Document's own Generate PDF setting (the app, not this node) so a PDF is
produced alongside the native Google Slides or Google Docs file. - Add a Google Drive distribution to the Document (or pick it in Deliver To). Once a
run finishes, its item'sdeliveriesarray holds one entry per destination; a Drive
delivery'sresultcarriesdrive_file_id/drive_urlfor the native file and, when
Generate PDF is on,pdf_file_id/pdf_urlfor the PDF right alongside them. - Use n8n's own Google Drive → Download node with
pdf_file_id(the same Google
credential you deliver with) to pull the PDF's bytes into the workflow — attach them, store
them, whatever needs the actual file rather than a link to it.
Google Drive is the Distribution to deliver through for this: it is the only
distribution type with a
delivery behind it today. Download exists as a type but is not enabled by default, and a
run delivering through one anyway records a failed delivery rather than a link.
Wait or poll
Wait For Completion (the default) is the simple path: the node polls GET /runs/{run}
itself and the next node only ever sees a finished result. It does this by holding the
workflow's execution open for as long as the run takes — for n8n running in queue mode,
that is a worker occupied for the whole wait, not just the request.
Turn it off for a document that reliably takes longer than a few polls, or in a workflow where
tying up a worker is the wrong trade: the node then returns as soon as the run is created,status: "pending" and no file_url yet. Poll it yourself afterward with Run → Get — on a
schedule, from a webhook your own system calls, or from a loop with a Wait node in between —
until status is one of succeeded, failed, partial_success or cancelled. For a split
Document, the same Run → Get on the batch's own id lists every child file once it is done
(or pass Summary Only to get the batch's own counters as a single item instead).
A run that times out inside Wait For Completion is not an error either: the item comes back
with timed_out: true and the run's id, ready for the same Run → Get poll. That holds even
when the last status check before Max Wait ran out meets a 429, a 5xx, a timeout or a dropped
connection: the item is timed_out, with the run as the node last saw it.
The real bounds. Max Wait defaults to 300 s, is never under Poll Every (so there is
always one poll) and never over 3600 s. Poll Every defaults to 5 s, is never under 2 s and
never over 3600 s. Both are read as numbers first: empty takes the default, and anything else
that is not a number (text, NaN, an object) is refused before the run is created, naming the
parameter, so a bad expression costs no run. Every request also times out after 30 seconds (a
poll after the smaller of that and what is left of Max Wait, with a floor of 5 s), so a server
that stops answering ends the wait within Max Wait plus one poll timeout. A poll that times out
is retried like a 429 or 5xx.
A transient answer while polling — a 429, a 5xx, a timeout or a dropped connection (a reset or
refused socket, a failed DNS lookup) — is retried within Max Wait on its own, honouring aRetry-After header when the API sends one (the Public API sends it on a 429). The wait is never
under 1 second, whatever Retry-After says (0 included), so a poll cannot spin; the startPOST's Retry-After wait has the same floor. The document read that Unknown Fields: Drop and Map
Columns make before the run, and the list of a batch's files after it, are retried on a 429
(and the other transient answers) in the same way, within Max Wait. A Retry-After longer than
what is left of Max Wait is handled differently by the two requests. The start POST does not
wait at all: the 429 is the error, with no second try. A poll sleeps out what is left of Max
Wait and polls once more; if that poll is transient too, the run is reported as timed_out, not
as an error. An error that is not transient (a 404, say) is an error, and any error raised once
a run exists — a poll that could not be retried, or the run itself ending failed — names that
run's id in its own message.
Run → Get waits only to read a batch's list of files, and it has its own Max Wait (300 s
by default) for that read: the same bounds as Generate's, never under 5 s (the default Poll Every)
and at most 3600. A single run and a Summary Only batch are read once and do not wait.
A 429 with the code authentication_rate_limited is different from the per-token limit: the
API has refused the API Token too many times from this address. Waiting does not help while
the token is wrong, so no wait or start request retries it; the message tells you to check the
API Token.
Every wait (Poll Every, a Retry-After, the start request's retries) ends as soon as the
execution is stopped, from the editor or by n8n's own execution timeout, instead of sleeping out
its interval.
Retry On Fail
Turn on n8n's Retry On Fail setting and a retry resumes the same run instead of starting
another:
- The automatic key. When Idempotency Key is empty, the node sends its own: it is the
same on every try of one run of the node, and different for each run of the node in an
execution and for each execution. A key you type is sent as it is. - As an AI tool the key comes from the request, on n8n's in-process tool path only. That
path (for example the MCP Server Trigger) raises the run index on every Retry On Fail try, so
there the tool variant ends its key with a hash of the request instead: the document and the
body without the key. Two identical tool calls in one execution on that path therefore share
one run, and the second returns the first's result withidempotent_replay: true. Where n8n's
engine runs the tool (the AI Agent node), each call has its own run index, the run index stays
the same across Retry On Fail tries, and the node uses it: a retry resumes the run, but two
identical calls start two runs. The regular node keeps the run index too, so a loop that sends
the same rows twice on purpose still starts two runs. - The automatic key adopts only a recent run. A conflict on the node's own key that names a
run created more than 6 hours ago is not adopted: it is treated like a failed run, and the
node starts a new one under the key followed by:after:and that run's id. (That happens
when n8n's database was reset or migrated and a key repeated; 6 hours is more than Retry On
Fail can span.) A key you type adopts its run whatever its age. - Adoption. A try whose start
POSTmet the first try's run gets422 run_idempotency_conflictnaming that run. The node does not treat that as an error: it reads
the run and carries on as if it had just created it — waits for it, or returns it — and the
output item saysidempotent_replay: true. A conflict that names no run stays an error. - The start
POSTretries by itself, inside a budget of three tries and Max Wait: a429always (no run was created), honouringRetry-After, except a429that refuses the API Token
(authentication_rate_limited); a5xx, a refused connection or
a timeout (a run may have been created) only while a key is sent, so the retry meets the
conflict and adopts the run. A4xxother than429is never retried. - After a failed run. A run that ended
failedis not adopted, whether or not it made its
document (see the edge case below), so a retry that meets one starts one new run under the key followed by:after:and the
failed run's id (a failed batch counts as failed only when every file failed, so a batch is
chained the same way). Every other status — pending, processing, succeeded, partial success,
cancelling and cancelled — is adopted. A key whose run was cancelled therefore keeps returning
that run withstatus: cancelled; to start again, change the key. - The edge case. A single run that rendered its document and then failed every delivery is
alsofailed. The retry starts a new run and renders a second file; the first one is left,
undelivered, in the render account. Checkdeliveriesbefore retrying such a run by hand. - "Retry execution" starts a new run. The button on the executions list creates a new
execution (n8n's own docs: the new one recordsretryOf), and the automatic key contains the
execution id, so it is a different key and generates a new document. Only Retry On Fail inside
one execution resumes.
Node versions and the Output option
The node has two versions. A workflow that already has the node stays on version 1, which
keeps the Simplify boolean and the output items exactly as they were. A node you add now is
version 1.1, which replaces Simplify with Output:
| Output | Generate and Run → Get return |
|---|---|
| Simplified (default) | the flattened fields: run_id, status, file_url, file_name, file_count, deliveries, warnings, generation_stats, folder_url, … |
| Raw | the same, plus the API's complete run object under run |
| Selected Fields | only the keys you tick under Fields (one is required; an unknown name is refused before anything is sent) |
Version 1.1 also adds, to the flattened item: warnings, generation_stats, folder_url (the
batch folder, from the run's destinations), document_id, processing_started_at,finished_at, and each delivery's distribution_id. A batch's children carry the batch'sfolder_url and generation_stats and their own warnings. Version 1 does not get these.
These keys are added after the selection, so they survive every Output choice and version:timed_out, idempotent_replay, dropped_keys, empty_columns, empty_rows andmissing_split_keys.
The three templates pin the node at 1.1 with Output Simplified.
Errors
Every non-2xx answer from the Public API surfaces as a node error whose message is the API's
own message and whose details carry its code (for example inline_column_unknown,run_idempotency_conflict or no_enabled_distributions), its category, any field it names,
and a next step: a link to the Run error codes reference
for a code, or what to check for a 401, 404 or 429. A parameter the node itself refuses
(a bad ID, Rows that are not JSON, a Max Wait that is not a number) is named in single quotes in
the message, and the details say what to change.
Two refusals have words of their own, because they come from a limit (see Limits). A
request body over 2 MiB (413, request_body_too_large) reads "The rows are too large to send in
one run" and a Data Set with too many rows (422, source_too_large) reads "A Data Set has more
rows than one run may hold". Both details say that nothing was started and to send fewer rows per
run: split them over several runs, or use a split document. A 413 from the web server in front
of the application (a plain-text answer over 40 MB) or a 502/503/504 is named for what it
is, rather than blamed on a wrong Base URL; only an answer this node genuinely cannot parse
points there.
The node's On Error setting changes what comes out, and the two item shapes differ:
- Continue (regular output) (and the older Continue On Fail switch) puts one flat item
on the success output, carrying every fact there is to carry:error(the message),error_code,errors(the envelope's own field names, such asunknown_keys,data_set_id
ortable_id),category, andrun_idwhenever a run was actually started before the failure. - Continue (using error output) puts
{ error, details }on the error output, wheredetails
holds the same facts. n8n merges the paired input item's own fields under it, so a read's item
also carries what it was reading (a Run → Get error carries therunfield it was given). For
Generate, which pairs to every input item, that is input item 0's fields.
The facts live in details there, not at the top level, because n8n rewrites any item that
carries its own error key and would drop them. A batch whose every file failed is expanded into
one item per failed file, each with that file's own run_id and error_code plus the
batch's own id under batch_run_id, in either shape. A read error names the failing item.
A run that ends failed raises the API's own error_message when it sent one. Only when the API
sent none does the message read "The run ended before the document was produced" (a batch: "The
batch run ended before all of its documents were produced"). The API's own error_code stays
verbatim.
The full, generated list of codes and what each one means is the product's own
Run error codes reference — it
is regenerated from the code that enforces them, so it stays current where this README would
not. A failed run (as opposed to a request the API itself refused) throws the same way, with the
run's own error_message as the message, its code and the same link in the details, and — for a
batch — the first few files that were not produced, with their own codes, alongside the batch's id.
When something is wrong or gone
- The Document was deleted (or its Project is in the trash, or the token belongs to another
workspace): the Data Set, Deliver To and Columns pickers show the API's404message ("Resource
not found.") instead of an empty list, and Generate throws the same404. A revoked token shows401("Invalid API token."). An empty picker means there is genuinely nothing to pick. - Deliver To holding a deleted or disabled Distribution is not an error: the API ignores that
id, answers201and delivers to the others. The node passes the ids through and adds no
warning. Only when none of them is valid does the start answer422 no_enabled_distributions.
Leave Deliver To empty to deliver to every enabled Distribution. - A Data Set deleted in the app and named in the rows is
422 inline_data_set_unknown
(errors.data_set_idnames it); the quota being used up is422 plan.limit_exceeded.generations
(errors.limit,errors.used). With Continue (regular output) each becomes one item carryingerror,error_code,categoryanderrors, and norun_id(no run was created). - The Document deleted between the start and the render: the run ends
failedwithrender_failedand "The document was deleted before this run could render it." (a batch
deleted before it is prepared:destination_container_failed). Deleted once the render has
begun, the run finishes as usual: it renders and delivers to every destination. Run → Get still returns the run either way. - No fields to send: the body goes out as JSON; "no fields" is
{},[]or no body. Any other
body that is not a JSON object, or an object sent as text or a form, is422onbody. - A cell of the wrong type is never refused: it renders as the column can show it, or empty
(a number column keeps12and"12", a boolean column only a realtrue, a date column an
ISO 8601 string with its offset dropped). Send dates as ISO 8601.
Use it as an AI tool
The node can be an AI Agent's tool, and every operation and parameter is described for an agent
to read cold. Whatever you hand to the model with n8n's "let the model define this parameter"
button ($fromAI) is chosen by the model, and n8n checks only the key name, so:
- Keep Document a literal. A model-chosen Document, Rows, Idempotency Key or Run ID can
point the run at any Document of the workspace (an Idempotency Key, see below, can also
return another call's run). The node refuses a value that is not a UUID,
but any real one passes. - Do not expose Deliver To to the model: it decides where files are sent.
- Put n8n's human review in front of Generate. From n8n 2.6, a tool's connection to the AI
Agent can carry an "Add human review step" that holds the call until a person approves it; use
it for Generate, which creates documents and counts against the plan. - Identical calls can share a run. On n8n's in-process tool path (for example the MCP Server
Trigger) the tool variant's automatic key comes from the request (see
Retry On Fail): two identical Generate calls in one execution start one run,
and the second returns it withidempotent_replay: true. Under the AI Agent node, which runs
the tool through n8n's engine, identical calls each start their own run. A fixed
Idempotency Key makes every call adopt the first run on either path; if you set one, it
must differ per call. A key adopts the run that already holds it whatever its rows and
whatever its age, so a key the model chooses by itself can return another conversation's run
(with its file links). Scope it to the call, so the model cannot name a key from another
execution:{{ $execution.id }}:{{ $fromAI('key', 'A short label for this document', 'string') }}. - Generate waits for the run (up to Max Wait, 300 s by default) inside one tool call. Turn
Wait For Completion off if the agent should not block, and let it poll with Run → Get.
Limits
- 60 requests a minute per API Token — Generate's own worst case is one start plus a poll
every 2 seconds (the minimum Poll Every allows), 31 requests a minute, under the limit but
not by much (32 when the run also reads the Document once, as Map Columns and Unknown Fields: Drop, the default, do); the default 5-second Poll Every is a gentler 12 a minute. A429carries aRetry-After, which the node honours inside Max Wait. - Every request times out after 30 seconds.
- A request body over 2 MiB is refused with
413(request_body_too_large), before the body
is read and before the token is checked. That is about 15 000 ordinary rows in one run. Over
40 MB the web server in front of the application answers first, in plain text. - A Data Set holds at most the plan's source-row limit (100,000 rows unless the plan sets
another). More rows sent in a run are refused at start with422source_too_large, and no run
is created. - For more rows than a run takes, split them over several runs, or use a split Document, which
makes one file per key from one request. The node's message for either refusal says the same.
Resources
- Templates: three importable workflows (weekly report from CRM deals, one
invoice per customer, generate a document from an n8n Form submission). - SourceToDocs documentation, in particular
Automate with n8n and the
Run error codes reference. - n8n community nodes documentation.
- Issues and source; building and testing the node is in
CONTRIBUTING.
Version history
- 1.0.0 — the first public release. The package ships node version 1 and node version 1.1:
a node you add is 1.1, and a workflow that already holds the node on version 1 keeps its
Simplify switch and its output items (see
Node versions and the Output option). The full list of
what the release contains is in the CHANGELOG.
License
MIT — © 2V Modules