Back to Nodes

SourceToDocs

Last updated Nov 6, 2026

n8n community node for SourceToDocs: generate Google Slides, Google Docs and PDF documents from the rows in your workflow, and read your projects, documents and runs through the SourceToDocs Public API

419 Weekly Downloads
570 Monthly Downloads

Included Nodes

SourceToDocs

Description

@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
  • Contributing
  • 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), whether it splits (Raw has its full 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 and Document → Get Many 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, Run → Get and (version 1.1) Document → Get offer Output (below) to choose how much
    each item carries; Document → Get at version 1 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 . 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 or
    302 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.

    1. 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.
    2. Add the SourceToDocs node, credential attached, Resource: Document,
    Operation: Generate.
    3. Pick the Document — From List (every document of the workspace, shown as
    Project / Document, searchable by either name; Options → Project narrows it to one project),
    or paste the document’s URL from the app
    (.../documents/show/), or its ID directly.
    4. 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.
    5. Leave Options → 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 carries
    id/createdTime, an n8n Form Trigger submission carries submittedAt/formMode — so the
    node removes anything that is not one of the Data Set’s own columns before sending, and
    reports what it removed as dropped_keys on 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 in
    dropped_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 own 422
    inlinecolumnunknown
    naming it, rather than being silently removed. Drop reads the
    Document once before the run (one extra GET) to learn each Data Set’s columns; Refuse
    sends the rows as given and does not read it.
    6. Open Options only when you need one of its fields. Leave Deliver to out to use the
    Document’s own distributions, or add it to pick a subset: the node sends the subset as
    distribution_ids, and leaving it out is the same as omitting that field from the Public API.
    A split Document ignores it and delivers through its own distributions, so the list shows a
    notice instead of a choice.
    7. Leave Wait for Completion on (the default; it is an option, so a node that never added it
    waits). Max Wait (300 s) and Poll Every (5 s), also under Options, control how
    long and how often the node polls before giving up; the bounds are in
    Wait or poll.
    8. Run it. The output item carries fileurl and filename at the top level — hand those
    straight to a Slack, email or HTTP node. Output (default Simplified) keeps the
    ten fields most workflows read; Raw adds the rest and the complete run object the API returned, under run; 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 (under Options) 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.)

    With Rows Come From: Connected Data Sources and more than one input item, the node shows a hint before it runs: it starts one run for all items, not one per item. For one document per item, put a Loop Over Items node before it.

    The node’s parameters, in order, for reference. Options is last and holds the optional fields, listed under it:

    | Parameter | Type | Default | Notes |
    | — | — | — | — |
    | 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 |
    | Simplify | boolean | true | node version 1 only; off keeps the full run object under run |
    | Output | options | Simplified | node version 1.1 and later: Simplified, Raw or Selected Fields; Document → Get has its own, see below |
    | Fields | multi-select | runid, status, fileurl | shown for Selected Fields only; runid (and batchrun_id on a batch’s files) is always kept, so a timed-out item can still be polled |
    | Options | collection | empty | the optional fields below; each is added with Add option and, left out, keeps its default |
    | → Additional Data Sets | collection | empty | shown for mapped only; JSON rows for other Data Sets |
    | → 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 |
    | → 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 |
    | → Max Wait | number (s) | 300, at least Poll Every, at most 3600 | hidden while Wait for Completion is off |
    | → Poll Every | number (s) | 5, at least 2, at most 3600 | hidden while Wait for Completion is off |
    | → Project | options | none | narrows the Document list above to one project; never sent to the API |
    | → Unknown Fields | options | Drop | shown for workflow and mapped; drop or refuse |
    | → Wait for Completion | boolean | true | |

    Run → Get takes a Run ID and the same Output (or Simplify, at version 1); its own Options collection holds Summary Only and, from node version 1.1, Max Wait.

    Document → Get has an Options collection too, holding only Project, which narrows the Document list as it does on Generate. Projects that share a name show the first four characters of their ID after the name (Sales · 3f2a), in the Project list and in the document names (Sales · 3f2a / Q3 Report). Typing in the Document list matches the whole name, with or without the ID part, so Sales, Q3 and Sales / Q3 all find it.

    Document → Get has an Output of its own from node version 1.1, with Fields offering the document’s own keys (id, projectid, projectname, name, type, splitenabled, generatepdf, createdat, updatedat, split, datasets, distributions; default id, name, datasets, and id always stays). Version 1 returns the raw document.

    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:

    1. 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.
    2. **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.
    3. 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.
    Options → 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. Options → Unknown Fields
    then works exactly as in JSON mode: Drop (the default) removes fields that are not the
    Data Set’s columns — an Airtable id/createdTime, a Form’s submittedAt — and reports
    them as droppedkeys; Refuse sends them and the API’s 422 inlinecolumn_unknown names 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 with
    file_count 0 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
    adds missingsplitkeys: [, …] to the output item and a hint.
    4. Rows for other Data Sets go in Options → Additional Data Sets, the same shape 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 is undefined or null in 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.

  • missingsplitkeys — 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 to
    flatMap over. The two tables have to come from two separate reads, joined only by a column
    your own data carries, not by n8n:

    1. 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.
    2. 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 on Get
    invoice lines
    (it would otherwise read once per customer). The template does exactly this and
    sets executionOrder to v1.
    3. 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 Options → Unknown Fields on Drop: both Airtable reads carry id/createdTime on 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.
    4. Wait for Completion on. The output is one item per customer’s file, each already
    carrying split_key (the driver value that file was generated for — the customer’s email,
    per step 1) alongside file_url.
    5. 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 .item on 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:

    1. 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.
    2. Add a Google Drive distribution to the Document (or pick it in Deliver to). Once a
    run finishes, its item’s deliveries array holds one entry per destination; a Drive
    delivery’s result carries drivefileid/drive_url for the native file and, when
    Generate PDF is on, pdffileid/pdf_url for the PDF right alongside them.
    3. Use n8n’s own Google Drive → Download node with pdffileid (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

    Options → 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 add Options → Summary Only to get the batch’s own counters as a single item instead: filecount under Simplified, and progress, folderurl and kind under Raw or Selected Fields).

    A run that times out inside Options → 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 a
    Retry-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 start
    POST‘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 Options → Max Wait (300 s
    by default, node version 1.1 and later) 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 authenticationratelimited 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 Options → Idempotency Key is left out or 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 with idempotent_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 POST met the first try’s run gets 422
  • runidempotencyconflict naming 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 says idempotent_replay: true. A conflict that names no run stays an error.

  • The start POST retries by itself, inside a budget of three tries and Max Wait: a
  • 429 always (no run was created), honouring Retry-After, except a 429 that refuses the API Token
    (authenticationratelimited); a 5xx, 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. A 4xx other than 429 is never retried.

  • After a failed run. A run that ended failed is 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 with status: cancelled; to start again, change the key.

  • The edge case. A single run that rendered its document and then failed every delivery is
  • also failed. The retry starts a new run and renders a second file; the first one is left,
    undelivered, in the render account. Check deliveries before 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 records retryOf), 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 flat output items it had in 1.0.0; the optional fields of
    Generate and Run → Get sit under Options in both versions (see the 2.0.0 note in the
    Version history). A node you add now is version 1.1, which replaces
    Simplify with Output:

    | Output | Generate and Run → Get return |
    | — | — |
    | Simplified (default) | exactly ten fields, in this order: runid, status, fileurl, filename, splitkey, filecount, deliveries, warnings, errorcode, errormessage; a batch’s file also keeps batchrun_id, the way back to its batch |
    | Raw | every flattened field (the ten, plus kind, pagecount, progress, generationstats, folderurl, documentid, processingstartedat, finished_at) and the API’s complete run object under run |
    | Selected Fields | only the keys you tick under Fields, any of the flattened fields above or run (one is required; an unknown name is refused before anything is sent) |

    Version 1.1 flattens warnings, generationstats, folderurl (the batch folder, from the
    run’s destinations), documentid, processingstartedat, finishedat, and each delivery’s
    distribution_id; Simplified keeps warnings and the deliveries, and Raw or Selected Fields give
    the rest. A batch’s children carry the batch’s folderurl and generationstats and their own
    warnings. Version 1 does not get these.

    Document → Get, from version 1.1:

    | Output | Document → Get returns |
    | — | — |
    | Simplified (default) | id, name, type, projectid, projectname, splitenabled, generatepdf, datasets as { id, name, columns } where columns is a list of column names, and distributions as { id, label, isenabled }. choices of a select column and the rest of each Data Set are not kept: use Raw for them. |
    | Raw | the document exactly as the API returned it |
    | Selected Fields | only the top-level keys you tick (default id, name, data_sets); id always stays |

    An expression such as datasets[0].columns[0].name therefore works under Raw and not under Simplified, where it is datasets[0].columns[0].

    These keys are added after the selection, so they survive every Output choice and version:
    timedout, idempotentreplay, droppedkeys, emptycolumns, empty_rows and
    missingsplitkeys.

    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 inlinecolumnunknown,
    runidempotencyconflict or noenableddistributions), 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, requestbodytoo_large) reads “The rows are too large to send in
    one run” and a Data Set with too many rows (422, sourcetoolarge) 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),
    errorcode, errors (the envelope’s own field names, such as unknownkeys, datasetid
    or tableid), category, and runid whenever a run was actually started before the failure.

  • Continue (using error output) puts { error, details } on the error output, where details
  • 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 the run field 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 runid and errorcode plus the
    batch’s own id under batchrunid, 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’s 404 message (“Resource
    not found.”) instead of an empty list, and Generate throws the same 404. A revoked token shows
    401 (“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, answers 201 and delivers to the others. The node passes the ids through and adds no
    warning. Only when none of them is valid does the start answer 422 noenableddistributions.
    Leave Deliver to empty to deliver to every enabled Distribution.

  • A Data Set deleted in the app and named in the rows is 422 inlinedataset_unknown
  • (errors.datasetid names it); the quota being used up is 422 plan.limit_exceeded.generations
    (errors.limit, errors.used). With Continue (regular output) each becomes one item carrying error,
    errorcode, category and errors, and no runid (no run was created).

  • The Document deleted between the start and the render: the run ends failed with
  • render_failed and “The document was deleted before this run could render it.” (a batch
    deleted before it is prepared: destinationcontainerfailed). 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, is 422 on body.

  • A cell of the wrong type is never refused: it renders as the column can show it, or empty
  • (a number column keeps 12 and "12", a boolean column only a real true, 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. n8n builds the tool from the operation’s name (for
    example “Generate document in SourceToDocs”) or the Tool Description you write, plus the
    description inside each $fromAI call: the texts in the node’s panel are for the person
    configuring it, and the model never reads them. So give every parameter you hand to the model
    its own description, for example:

  • Rows: {{ $fromAI('rows', 'A JSON array of objects, one per row, with keys matching the Data Set column names', 'json') }}
  • Run ID (Run → Get): {{ $fromAI('runid', 'The runid returned by a Generate call', 'string') }}
  • Idempotency Key: see below.
  • 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 Options → 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 with idempotent_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
  • Options → 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. A 429 carries a
    Retry-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 (requestbodytoo_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 with 422 sourcetoolarge, 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

  • 2.0.1 — the nodes panel finds the node by its name, SourceToDocs or Source to Docs,
  • and no longer by generic terms such as “invoice” or “Google Slides”. Nothing else changes.

  • 2.0.0 — Generate’s optional fields (Deliver to, Wait for Completion, Max Wait, Poll Every,
  • Idempotency Key, Unknown Fields, Additional Data Sets) and Run → Get’s (Summary Only, Max Wait)
    moved into an Options collection, in both node versions. A node saved with 1.0.0 loses the
    values it held for them and runs on the defaults, so set them again under Options. The
    Document picker lists the whole workspace, and Document → Get and Generate’s Project filter moves
    into Options; Simplified output keeps ten fields; Document → Get gains an Output option; and a hint
    says Generate starts one run for all input items.

  • 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

From our network Other tools and sites we build