n8n-nodes-adeu
v3.0.6
Published
n8n community node for Adeu — apply AI-driven tracked changes, generate diffs, and sanitize Microsoft Word (.docx) documents.
Maintainers
Readme
n8n-nodes-adeu
An n8n community node for Adeu — the AI-native Virtual DOM for Microsoft Word.
🆕 New in this release:
Extract Outline— a new operation returning a token-cheap structural map (headings + page numbers + table flags) for navigating large documents. Each heading also carriesend_page, the last page it owns, so you can read a whole section with onepage–end_pagerange instead of guessing.Apply Text Revision— a new operation that takes the complete revised clean text of a document and writes the difference against its own clean view as native Word tracked changes, for when an LLM rewrote the document wholesale and no per-change anchors are left.Pageparameter onExtract Markdown— fetch only one page of a paginated projection instead of the whole document.match_modeandregexonmodifyedits —Apply Editsnow supports targeted multi-occurrence writes. Setmatch_mode: "all"to replace every occurrence,"first"to anchor to the first hit silently, or omit/"strict"to fail on ambiguity. Setregex: trueto interprettarget_textas an ES2022 RegExp (with$1,$2capture-group references innew_text).Allow Partial ApplicationonApply Edits— opt-in salvage batches: valid edits are applied and saved, the item'sstatusreads"partial", and rejected edits come back instats.failedfor an agent to fix and resubmit.Existing workflows must hand-update their
$fromAIexpressions to expose the new fields — n8n caches$fromAIexpressions per workflow and does not retroactively update them on package upgrades.The shipped example workflows in
examples/are kept in sync with these recipes — re-import them after upgrading rather than hand-patching an old copy.
This node bridges the gap between Large Language Models (LLMs) and Microsoft Word. It translates complex OpenXML (.docx) files into token-efficient Markdown, allows AI models to reason over legal or technical text, and translates the AI's JSON output back into native Word Tracked Changes and Comments — all completely in-process, without your documents ever leaving the n8n runtime.
📦 Installation
Depending on your self-hosted n8n configuration, you can install this node via the UI, environment variables, or manually.
Method 1: GUI Installation (Recommended)
- In n8n, go to Settings > Community Nodes.
- Select Install.
- Enter
n8n-nodes-adeuin the Enter npm package name field. - Check I understand the risks of installing unverified code from a public source.
- Select Install.
Method 2: Environment Variables
For automated deployments, you can bootstrap your n8n instance with a fixed set of packages via environment variables:
export N8N_COMMUNITY_PACKAGES_MANAGED_BY_ENV=true
export N8N_COMMUNITY_PACKAGES='[{"name":"n8n-nodes-adeu"}]'Note: Enabling this makes the Community Nodes settings UI read-only and will automatically uninstall any packages not listed in the JSON array.
Method 3: Manual Installation (Queue Mode)
If your n8n instance runs in queue mode or you prefer terminal installation, you can install the node manually:
docker exec -it n8n sh
mkdir -p ~/.n8n/nodes
cd ~/.n8n/nodes
npm i n8n-nodes-adeuRestart your n8n instance after installation.
🚀 Key Capabilities
- CriticMarkup Projection: Translates existing Word tracked changes into standard Markdown (
{++inserted++},{--deleted--}). - Semantic Appendix: Automatically extracts defined terms, cross-references, and potential typos to give LLMs deeper context.
- Structural Outline: Lightweight headings-and-pages map of any document, with table/footnote flags per section.
- Pagination: Drill into a single page of a large document instead of blasting the full body into LLM context.
- Native Redlining: Apply
modify,accept,reject, andreplyactions directly to the OOXML tree. - Whole-Text Revision: Hand back the complete revised clean text and let the engine diff it against the document and write the tracked changes, behind a post-apply verification gate.
- Structured Diffs: emit a diff as a ready-to-apply
DocumentChangearray, not just text. - Salvage Batches: Optionally keep the changes that validate and get every failure reported with its 0-based index, instead of an all-or-nothing rejection.
- Targeted Multi-Occurrence Writes:
match_mode(strict/first/all) andregexsupport for surgical or sweeping replacements. - Document Sanitization: Strip metadata, auto-accept markup, and apply read-only locks before sending to counterparties.
⚙️ Operations
The node exposes one resource (Document) with seven operations:
1. Extract Markdown
Projects a .docx file into LLM-friendly Markdown.
- Input:
.docxbinary. - Output: JSON
{ markdown, fileName, cleanView }(plus pagination metadata when apageis requested). - Clean View toggle:
False(Raw View): Shows all pending tracked changes via CriticMarkup. Best for resolving counterparty edits.True(Clean View): Simulates an "Accept All" state, hiding markup. Best for generating net-new redlines on a clean baseline.
- Include Appendix (boolean, default
true): whether to append the Structural Appendix (defined terms, cross-reference anchors, potential typos). Turn it off to save context when you only need body text. - 🆕 Page parameter: Optional 1-based page number. When
0(default), the full document is returned. When>= 1, only that page's content is returned and the JSON includes{ page, total_pages, has_next, has_prev, tracked_change_count }. Pages are ~19,000-character chunks of the projected body; the Structural Appendix is appended to every page. Use Extract Outline first to discover how many pages exist.
2. Extract Outline 🆕
Returns a token-cheap structural map of the document — essentially a table of contents an LLM can use to navigate large files.
- Input:
.docxbinary. - Output: JSON
{ fileName, total_pages, outline: OutlineNode[] }where eachOutlineNodeis:{ "level": 2, "text": "Confidentiality", "page": 1, "end_page": 3, "style": "Heading 2", "has_table": false, "footnote_ids": ["fn-1", "fn-3"] }level(1–6): Heading depth.text: Heading text with markdown/CriticMarkup stripped.page: Which Extract Markdown page this heading lands on.end_page: Last Extract Markdown page this heading owns — the page before the next heading of the same or shallower level (equal topagefor single-page sections). Usepage–end_pageto read a whole section without guessing.style: Word style name (e.g.Heading 1,Title) or(heuristic)for headings detected purely by typography.has_table: Whether the section directly contains a Word table (does not bubble up to ancestor headings).footnote_ids: Footnote/endnote markers scoped to this section, in document order, e.g.fn-1,en-2.
- Typical pattern: Call this first, let the LLM choose a section, then call Extract Markdown with the matching
pageto get just that page's content.
3. Apply Edits
Applies a JSON array of DocumentChange operations back to the Word document as tracked changes and comments.
- Input:
.docxbinary + achangesJSON array (read from an upstream node or defined inline). - Output: A new redlined
.docxbinary + JSON application stats with per-edit reports (status, occurrences modified, heading path, pages affected, CriticMarkup context, post-accept preview). - Atomic Batch Validation (default): Adeu pre-validates the entire array of edits before touching the document. If even one edit is invalid (e.g., target text not found, ambiguous match), the engine safely rejects the entire batch to prevent partial or corrupted document states.
- Allow Partial Application (opt-in salvage mode, default
false): Set this boolean on the node to keep the edits that validate and report the ones that fail instead of rejecting the whole batch. See Transactional Batches below.
🆕 Targeted Multi-Occurrence Writes (match_mode + regex)
The modify edit type now supports two optional fields:
match_mode("strict"|"first"|"all", default"strict"):"strict": Fails with an actionable ambiguity error iftarget_textmatches more than one location. Recommended default — surfaces ambiguity to the LLM so it can self-correct with more context."first": Silently anchors to the first occurrence in linear document order. Use only when you've verified there's just one intended hit."all": Applies the same replacement to every occurrence. Returnsoccurrences_modifiedin the per-edit report. Pages listed in the report cover all modified locations.
regex(boolean, defaultfalse):- When
true,target_textis interpreted as an ES2022RegExppattern (case-sensitive by default — embed flags via inline syntax like(?i)if needed). new_textmay reference capture groups via$1,$2, etc.- Combine with
match_mode: "all"for global regex-based replacements.
- When
Example — convert all dollar amounts to EUR:
[
{
"type": "modify",
"target_text": "\\$(\\d+)",
"new_text": "EUR $1",
"match_mode": "all",
"regex": true,
"comment": "Currency normalization."
}
]🔍 Transactional Batches (Self-Correction for AI Agents)
By default (Allow Partial Application off), Apply Edits is all-or-nothing: if any single edit fails validation (target text not found, ambiguous match, read-only target, overlapping another author's change), the engine throws a BatchValidationError atomically and the document is left untouched. The error message names the failing edit and explains why it failed, so an agent can correct just that edit and re-call — no separate preview round trip is needed.
- Failure blame: When a batch is rejected, the thrown error's description names each failing change by its 0-based index and repeats the two-call recovery protocol (re-apply the batch without the failing changes, then fix those separately in a small batch).
- Author impersonation warning:
stats.author_impersonation_warningis set when theAuthoryou configured matches an author who already has pending revisions in the document — a redline that would be indistinguishable from theirs in Word's review pane. - Stale ids: If an
accept/reject/replynames an id that no longer exists, the error lists the ids the document actually has and tells you to re-run Extract Markdown, because ids shift between document states.
🩹 Salvage Mode (Allow Partial Application) 🆕
Turning the node's Allow Partial Application boolean on deliberately suspends that transactional guarantee for the call:
- Every edit that validates is applied and saved — nothing is rolled back because a later edit failed.
- The output item's top-level
statusreads"partial"instead of"ok", so a downstream If node can branch on it without digging intostats. stats.failedlists each rejected edit as{ index, reason, error }, whereindexis the edit's 0-based position in the array you submitted.stats.edits_applied/stats.edits_skippedgive the totals.- Failed edits (and
accept/reject/replyactions whosetarget_idis stale) no longer throw, so the node does not halt — the redlined.docxbinary is still produced from the edits that landed. - Malformed batches are still rejected outright, even in salvage mode: pre-flight shape errors on review actions (an empty
replytext, two contradictory resolutions of the sametarget_id) fail the whole call, because they signal a broken batch rather than an edit that missed its anchor.
Use it for AI Agent loops that will read stats.failed, fix the rejected edits, and resubmit them against the returned draft (pass redlinedBinaryId back in as Source Binary ID). Do not use it where a half-applied redline could be shipped as final — leave the default off and let the batch fail loudly instead.
4. Apply Text Revision 🆕
Applies a whole revised document text as tracked changes.
- Input:
.docxbinary +Revised Text— the complete clean text of the document. - Author (default
Adeu AI): author name attached to every tracked change produced by this revision. - Output Binary Property (default
data): property name on the outgoing item to receive the redlined.docxfile. - Output: A new redlined
.docxbinary onOutput Binary Property+ JSON{ fileName, author, stats, reasoning?, redlinedBinaryId? }. Both trailing keys are conditional:reasoningis echoed back verbatim whenever the Reasoning field is non-empty (the$fromAIrecipe below binds it, so agent calls normally receive it) and is omitted when it is blank;redlinedBinaryIdis present only under AI Agent tool execution, for cumulative multi-turn editing. - How to produce
Revised Text: Call Extract Markdown withClean Viewon andPage0, edit that text, and send all of it back. The engine diffs it against the document's own clean view and writes the difference as native Word tracked changes. - Refusals (nothing is applied):
- CriticMarkup tags (
{++ ++},{-- --},{>> <<}) in the text — the tool compares against the clean view, so markup would land as literal prose. - A single page of a paginated extract — everything absent from the text would be applied as a tracked deletion.
- A revision deleting more than 50% of the characters (75% under 2,000 characters), unless
Allow Major Deletionsis on.
- CriticMarkup tags (
- Verification gate: after applying, the engine re-reads the document's clean text and compares it with what you sent. If they differ, the operation fails and no binary is returned — structure such as headings, table rows, and footnotes cannot be removed by text replacement.
- Apply Edits vs Apply Text Revision: use Apply Edits when you have targeted changes with anchors; use Apply Text Revision when an LLM rewrote the document wholesale and you no longer have per-change anchors.
5. Generate Diff
Produces a sub-word level @@ Word Patch @@ diff, a unified diff, or structured changes between two versions of a document.
- Input: two
.docxbinaries. Each side has its own source:Original Document Source/Original Source Node Name/Original Binary Property(defaultdata) andModified Document Source/Modified Source Node Name/Modified Binary Property(defaultdata2). InFrom Connected Inputmode the two binary property names must differ. - Clean View (boolean, default
true, unlike Extract Markdown'sfalse): compare the Accept-All view of both documents. Setfalseto diff the CriticMarkup projection and audit the tracked changes themselves. - Diff Format (default
Word Patch):Word Patch(wordPatch) — Adeu@@ Word Patch @@sub-word text diff. Output JSON{ originalFileName, modifiedFileName, cleanView, diffFormat, diff, warnings? }.Unified Diff(unified) — Git-style unified text diff, same output shape.Structured Changes (JSON)(structuredChanges) — a JSON array ofDocumentChangeobjects that transform original into modified, returned onchanges(notdiff) so it can be piped straight into Apply Edits. Output JSON{ originalFileName, modifiedFileName, cleanView, diffFormat, changes, warnings? }.
- Identical documents: both text formats return an explicit
No textual differences found between the documents.body rather than an empty string. - Media warning: a text diff cannot see image bytes, so when embedded media differ the
warningsarray is populated and the warning text is prefixed ontodiff.
6. Finalize
Prepares a document for signature or external distribution.
- Input:
.docxbinary. - Output: A new finalized
.docxbinary onOutput Binary Property(defaultdata) + JSON{ fileName, sanitizeMode, report, reasoning? }. - Reasoning: Optional audit-only text explaining why the document is being finalized now. Never forwarded to the engine.
- Modes:
Full: Strips all metadata and requires all tracked changes/comments to be resolved (or auto-accepted viaAccept All Tracked Changes). IfacceptAllisfalseand pending changes exist, the operation throws rather than producing a file.Keep Markup: Strips metadata but preserves visible tracked changes and comments. Allows you to override theAuthorname viaAuthor Override(e.g., change "Adeu AI" to "My Law Firm").Baseline: Only strips background noise (RSIDs, proof errors) without touching metadata.
- Accept All Tracked Changes: (boolean, default
false) only shows and applies underSanitize Mode: full. - Author Override: (string, optional) only shows and applies under
Sanitize Mode: keep-markup. - Protection: Can inject a native Word "Read-Only" lock into the document settings (
noneorread_only).
7. Hydrate Tool Output (The "Hydration" Note)
Because n8n's AI Agent tool wrapper intercepts and strips all binary data from tool outputs, files generated inside an AI loop cannot reach downstream nodes directly.
- What it does: This operation is placed immediately downstream of the AI Agent on the main workflow execution line. It reads the stashed metadata pointer (
adeu_last_redlined) left by the last execution of a redline operation (Apply EditsorApply Text Revision), retrieves the raw file stream directly from n8n's secure binary storage, and attaches a fresh binary buffer onto the outgoing item. - Parameters:
- Static Data Key (default
adeu_last_redlined): key in workflow global static data to read the stashed binary metadata pointer from. - Output Binary Property (default
data): property name on the outgoing item to receive the hydrated binary. - On Missing (
Emit Empty|Throw, defaultEmit Empty):Emit Emptyemits{ hydrated: false }with no binary so a downstream If node can gate the write;Throwraises aNodeOperationError(for deterministic pipelines where the stash must be present). - Clear After Read (boolean, default
true): deletes the stash entry after successful hydration so it cannot leak into the next run. - Output Path Template (optional string): template to compute the final write path on disk, returned on the output JSON as
outputPath. Supports placeholders:{baseName}(filename with extension stripped),{timestamp}(ISO 8601 with:and.replaced by-),{fileName}(full filename), and{ext}(e.g..docx).
- Static Data Key (default
- Output: JSON
{ hydrated: true, fileName, sourceBinaryId, mimeType, outputPath? }(or{ hydrated: false }when missing underEmit Empty) + hydrated binary onOutput Binary Property. - Note: This operation is never used as an AI Agent tool, so it has no
$fromAIrecipes — configure it in the node editor.
🧠 The DocumentChange Schema
To use the Apply Edits operation, your LLM must output a JSON array of objects matching this schema.
| Type | Required Fields | Optional Fields | Description |
| :--- | :--- | :--- | :--- |
| modify | target_text, new_text | comment, match_mode, regex | Replaces baseline text. match_mode: "strict" (default, fails on ambiguity), "first" (silently picks first hit), "all" (replaces every occurrence). regex: when true, target_text is an ES2022 RegExp pattern. |
| accept | target_id | comment | Accepts an existing tracked change (e.g., Chg:123). |
| reject | target_id | comment | Rejects an existing tracked change. |
| reply | target_id, text | — | Replies to an existing comment (e.g., Com:456). |
| insert_row | target_text, position, cells | — | Inserts a new table row above or below the target cell text. |
| delete_row | target_text | — | Deletes the table row containing the target text. |
Example LLM Output:
[
{
"type": "reject",
"target_id": "Chg:12",
"comment": "We cannot accept 60-day terms."
},
{
"type": "modify",
"target_text": "within thirty (30) days",
"new_text": "within forty-five (45) days",
"comment": "Compromise per our playbook."
},
{
"type": "modify",
"target_text": "the Contractor",
"new_text": "the Service Provider",
"match_mode": "all",
"comment": "Term harmonization."
}
]🔄 Handling Cumulative & Multi-Turn Edits (The Binary ID Pattern)
When an AI Agent applies edits, receives feedback, and needs to make another round of changes, loading from the original node name (e.g., 'Read Binary File') would discard the modifications just made. To allow the model to chain consecutive edits seamlessly, the node utilizes an explicit state pointer pipeline:
- First Tool Call: The LLM loads from the baseline. It sets
Source_Node_Nameto the canvas node (e.g.,'Read Binary File') and leavesSource_Binary_Idblank. - Intermediate Output: The redline tools (
Apply EditsandApply Text Revision) apply changes and return a uniqueredlinedBinaryId(representing the immutable state of that edit) back in the JSON payload to the LLM. - Subsequent Tool Calls: If the LLM needs to make further changes on top of its prior work, it must set
Source_Binary_Idto the ID string returned by the previous call. The node's backend dynamically detects this ID, bypasses the upstream node name, and pulls the intermediate document directly from storage to apply the new changes cumulatively. - Handoff: On every successful execution of a redline operation (
Apply EditsorApply Text Revision), the node overwrites a global static pointer (adeu_last_redlined) with the newest ID. When the AI Agent finishes its entire chat turn, the downstreamHydrate Tool Outputnode reads this pointer to output the final, fully-cumulative document.
🏗️ Typical Pipeline
[ Gmail Trigger (Incoming Doc) ]
│
▼
[ Adeu: Extract Outline ] ← Cheap structural map for large documents
│
▼
[ Adeu: Extract Markdown ] ← Optionally page-scoped via Page parameter
│
▼
[ AI Node (LLM) ] ← Outputs a JSON array of DocumentChange objects
│
▼
[ Adeu: Apply Edits ] ← Pre-validates and writes redlines atomically
│
▼
[ Gmail: Reply with Doc ]💡 Prompting Best Practices for LLMs
To achieve the highest batch success rate when prompting models like Gemini, GPT-4o, or Claude to generate edits:
- Enforce Exact Matching: Instruct the LLM: "The
target_textmust be copied EXACTLY from the source document — including identical punctuation, spacing, and capitalization." - Short but Unique: Instruct the LLM: "Keep
target_textshort, but ensure it is unique enough to not match multiple locations in the document. If you need to replace the same phrase in many places, usematch_mode: 'all'instead of writing multiple separate edits." - No Fake Markup: Instruct the LLM: "Do NOT include CriticMarkup tags like
{++or{--in yournew_text. The engine will apply the redline tracking automatically." - Mind the Overlap Constraint: Adeu's engine strictly prevents
modify(text-replace) edits from overlapping with or targeting text that is already inside a pending tracked change. Instruct the LLM: "You cannotmodifytext that is wrapped in counterparty tracking markup. You mustacceptorrejecttheir change using its ID." - Use Outline for Navigation: For documents longer than ~20 pages, instruct the LLM to call
Extract Outlinefirst to get a structural map, then callExtract Markdownwith a specificPagenumber to drill in. This avoids blowing the context window on the full document body.
🤖 AI Agent Tool Setup: $fromAI Recipes
How the tool node is created
This package ships exactly one node class. Because it declares usableAsTool: true, n8n generates the AI-tool variant itself at load time: it appends Tool to the node's name, empties its main input, and gives it a single ai_tool output. There is no source file, no separate export, and nothing to install for the tool version. Consequences worth knowing:
- In an exported workflow JSON the two legal
typevalues aren8n-nodes-adeu.adeu(main line) andn8n-nodes-adeu.adeuTool(connected to an AI Agent'sai_toolport). Both aretypeVersion: 1. - n8n also injects two parameters that exist only on the tool variant: Tool Description (
descriptionType,auto|manual, defaultauto) and Description (toolDescription). LeavedescriptionTypeonautoand the model receives only a stub like "Apply tracked changes to document in Adeu". Set it to Set Manually and write a real description — the tool description is what makes the model choose the right tool at all, and it is separate from the per-parameter$fromAIdescriptions below. - Every tool node must keep at least one
$fromAI()binding. Google Gemini rejects a tool with zero dynamic parameters outright ("Google Gemini requires at least one dynamic parameter when using tools"). Hydrate Tool Outputis the exception: it is never a tool. It runs on the main line after the Agent.
When wiring this node into an AI Agent as a tool, n8n auto-generates $fromAI() expressions for AI-bindable fields. The second argument of $fromAI is the only per-parameter schema description the LLM actually receives — but n8n does not propagate node-source description metadata into that slot. Auto-generated stubs look like:
{{ $fromAI('Changes__JSON_', ``, 'string') }}The empty backticks mean the LLM sees no schema for that field and will hallucinate the structure.
To apply any recipe below: Open the tool node → click the target field → disable "Let the model define this parameter" (it locks the field to n8n's auto-generated empty-description stub) → switch to Expression mode → paste the recipe. The $fromAI() call inside your expression still binds the field to the LLM — you're just bypassing the auto-stub so you can supply a richer schema description.
Stub caching gotcha: Once a
$fromAIexpression is saved into a workflow, n8n caches it permanently in that workflow's JSON. Updating this package does not retroactively update expressions in existing workflows — you must hand-edit them or delete and re-add the tool node.
What you do NOT bind to the LLM
Some fields are plumbing — they configure which input/output port the node uses, not semantic content. Plumbing fields belong in the node editor; binding them to $fromAI lets the LLM produce confusing errors unrelated to the user's actual request.
Set these manually in the node editor — do NOT use $fromAI:
Document Source(fromInputvsfromNode) — workflow topology decision.Input Binary Property— wiring decision; downstream of the source node, not per-call.Output Binary Property(Apply Edits, Apply Text Revision, Finalize) — names where the outgoing binary lands on the workflow item. Downstream nodes need this fixed; an LLM pickingoutput_dataone call andresultthe next would break the pipeline. Default'data'is almost always correct.Edits Source(Apply Edits) — controls whether the node reads the changes array from theChanges (JSON)field on the node itself (defineBelow) or from a property on the upstream item (fromInputJson). For AI Agent workflows, set this toDefine Belowin the editor. This is what activates theChanges (JSON)field as the LLM's entry point — the recipe in the Apply Edits section below populates that field via$fromAI, and the LLM hands its generatedChanges_JSONstring directly to the tool as a call argument. ThefromInputJsonbranch is only for deterministic pipelines where an upstream non-AI node has pre-populated achangesproperty on the item.Allow Partial Application(Apply Edits) — decides whether a flawed batch is rejected wholesale or half-applied. That is your risk policy for the workflow, not a per-call choice; an LLM that can switch it on will switch it on to make its own errors disappear. Leave itfalseunless the workflow is an agent loop that resubmits the failures.Allow Major Deletions(Apply Text Revision) — decides whether a revision deleting >50% of the document is allowed. That is your risk policy for the workflow, not a per-call choice. Leave itfalseunless whole-document truncation is intended.Original Binary Property/Modified Binary Property(Generate Diff) — wiring, same reasoning asInput Binary Property.Static Data Key,On Missing,Clear After Read,Output Path Template(Hydrate Tool Output) — the whole operation is main-line only.Tool Description/Description(descriptionType/toolDescription) — these describe the tool to the model; binding them to the model is circular.
AI Agents cannot pass binary .docx data through JSON arguments anyway — that's why fromNode exists: it resolves the binary from a named upstream node (e.g. Read Binary File, Gmail Trigger) at execution time. The trigger source is $fromAI-bindable below because a system prompt can legitimately offer the LLM a choice between multiple binary-producing nodes.
Extract Markdown
Source Node Name (when Document Source is From Another Node):
={{ $fromAI('Source_Node_Name', `Exact name of the workflow node that produced the .docx binary (string, case-sensitive, e.g. 'Read Binary File' or 'Gmail Trigger'). Must match the node label in the canvas exactly. If your system prompt specifies which node holds the document, always use that name.`, 'string', 'Read Binary File') }}Source Binary ID (when Document Source is From Another Node):
={{ $fromAI('Source_Binary_Id', `Optional string. If you are inspecting a document that you have already modified during this conversation, pass the 'redlinedBinaryId' from the previous tool output here to view the updated draft. Leave empty on the first call to load from the baseline node name.`, 'string', '') }}Clean View:
={{ $fromAI('Clean_View', `Boolean. Set false (default) to surface all pending tracked changes as CriticMarkup tags {++ins++}, {--del--}, {>>comment<<} — use when reviewing counterparty edits or any document with pending markup. Set true to project the document as if all tracked changes were accepted (simulates Accept All) — use only when generating net-new redlines against a clean baseline.`, 'boolean', false) }}Include Appendix:
={{ $fromAI('Include_Appendix', `Boolean, default true. Whether to append the Structural Appendix — the list of defined terms, cross-reference anchors, and likely typos found in the document. Keep true when you need to reason about definitions or internal references. Set false only to save context when you want body text alone.`, 'boolean', true) }}Page 🆕:
={{ $fromAI('Page', `Optional 1-based integer page number to retrieve only one page of the projected document. Set to 0 (default) for the full document body — use 0 for short documents (under ~10 pages). For long documents, call extract_outline first to discover total_pages and which headings live on which page, then call this tool again with Page set to the page you need. Pages are ~19,000-character chunks; the Structural Appendix is appended to every page. If you request a page beyond total_pages the tool will error.`, 'number', 0) }}Extract Outline 🆕
Source Node Name (when Document Source is From Another Node):
={{ $fromAI('Source_Node_Name', `Exact name of the workflow node that produced the .docx binary (string, case-sensitive, e.g. 'Read Binary File' or 'Gmail Trigger'). Must match the node label in the canvas exactly.`, 'string', 'Read Binary File') }}Source Binary ID (when Document Source is From Another Node):
={{ $fromAI('Source_Binary_Id', `Optional string. If you are inspecting a document that you have already modified during this conversation, pass the 'redlinedBinaryId' from the previous tool output here to view the updated draft outline. Leave empty on the first call to load from the baseline node name.`, 'string', '') }}Apply Edits
Reasoning (fill this FIRST):
={{ $fromAI('Reasoning', `State your reasoning for this batch of edits BEFORE you produce the Changes_JSON array: briefly explain what you intend to change and why (e.g. which clauses, which counterparty positions you are countering, which playbook rule applies). Always write this field first — reasoning through the change before emitting the JSON produces more accurate, better-anchored edits. This text is captured for audit only and does not alter engine behavior. One to three sentences is enough.`, 'string', '') }}Source Node Name (when Document Source is From Another Node):
={{ $fromAI('Source_Node_Name', `Exact name of the workflow node that produced the .docx binary (string, case-sensitive). Must match the node label in the canvas exactly. If your system prompt specifies which node holds the document, always use that name.`, 'string', 'Read Binary File') }}Source Binary ID (when Document Source is From Another Node):
={{ $fromAI('Source_Binary_Id', `Optional string. If you are doing consecutive edits on the same document during this conversation, pass the 'redlinedBinaryId' from the previous tool output here to continue editing the updated draft. Leave blank on your first tool call.`, 'string', '') }}Author:
={{ $fromAI('Author', `Author name attached to every tracked change and comment produced by this batch (string, e.g. 'AI Reviewer' or 'Acme Legal AI'). Appears in Word's review pane as the author of every redline. Choose a name your end users will recognize as the AI reviewer.`, 'string', 'Adeu AI') }}Changes (JSON):
={{ $fromAI('Changes_JSON', `JSON-encoded string containing an array of DocumentChange objects. Each object is one of: {"type":"modify","target_text":"<verbatim from source>","new_text":"<replacement>","comment":"<optional>","match_mode":"<optional 'strict' (default) | 'first' | 'all'>","regex":<optional boolean default false>} | {"type":"accept","target_id":"Chg:12","comment":"<optional>"} | {"type":"reject","target_id":"Chg:12","comment":"<optional>"} | {"type":"reply","target_id":"Com:45","text":"<reply>"} | {"type":"insert_row","target_text":"<cell text anchoring row>","position":"above" or "below","cells":["col1","col2"]} | {"type":"delete_row","target_text":"<cell text anchoring row>"}. MODIFY EXTENDED: set match_mode='all' to replace every occurrence of target_text in linear document order (returns occurrences_modified in the per-edit report); set match_mode='first' to silently anchor to the first hit; omit or use 'strict' (default) to fail on ambiguous matches so you can self-correct with more context. Set regex=true to interpret target_text as an ES2022 RegExp pattern; new_text may reference capture groups via $1, $2 etc. Combine match_mode='all' with regex=true for global pattern-based replacements. RULES: target_text must be copied VERBATIM from the source including punctuation/whitespace/case (unless regex=true) and must uniquely anchor one location under match_mode='strict'; never include CriticMarkup tags like {++ or {-- in new_text — the engine applies tracking automatically; use Chg:N and Com:N IDs exactly as surfaced by extract_markdown; the entire array must be a single JSON-encoded string. Atomic batch: if any single edit is invalid the whole array is rejected with an error telling you which edit failed — use that to self-correct on the next call. (If the node has 'Allow Partial Application' enabled, the valid edits are applied instead and the rejected ones come back in stats.failed with their 0-based index — fix those and resubmit them against the returned draft.)`, 'string') }}Return Markdown Output:
={{ $fromAI('Return_Markdown', `Boolean. When true (default), the tool returns the post-edit document as Markdown with CriticMarkup so you can verify what changed and reason about follow-up edits. Set false only to skip extraction when you are confident no follow-up review is needed.`, 'boolean', true) }}Apply Text Revision 🆕
Reasoning (fill this FIRST):
={{ $fromAI('Reasoning', `State your reasoning for this revision BEFORE providing the revised text: what you intend to change, why the changes are being made, and which sections or clauses are affected. Always write this field first. This text is captured for audit only and does not alter engine behavior. One to three sentences is enough.`, 'string', '') }}Source Node Name (when Document Source is From Another Node):
={{ $fromAI('Source_Node_Name', `Exact name of the workflow node that produced the .docx binary (string, case-sensitive). Must match the node label in the canvas exactly. If your system prompt specifies which node holds the document, always use that name.`, 'string', 'Read Binary File') }}Source Binary ID (when Document Source is From Another Node):
={{ $fromAI('Source_Binary_Id', `Optional string. If you are doing consecutive revisions on the same document during this conversation, pass the 'redlinedBinaryId' from the previous tool output here to continue editing the updated draft. Leave blank on your first tool call.`, 'string', '') }}Author:
={{ $fromAI('Author', `Author name attached to every tracked change this revision produces (string, e.g. 'AI Reviewer' or 'Acme Legal AI'). Appears in Word's review pane as the author of each redline.`, 'string', 'Adeu AI') }}Revised Text:
={{ $fromAI('Revised_Text', `The COMPLETE revised clean text of the document. Read it first with Extract Markdown using Clean View on and Page 0 (whole document), edit that text, then send all of it back — the engine diffs this against the document's clean view and turns the difference into tracked changes. Never include CriticMarkup tags like {++ or {-- in the text — they are rejected. Never send a single page of a paginated extract — everything missing from this text is applied as a tracked deletion. A revision deleting more than 50% of the characters (75% under 2,000 characters) is refused unless Allow Major Deletions is on. After applying, the engine verifies that the document's clean text matches this value; if they differ, the operation fails.`, 'string') }}Generate Diff
Original Source Node Name (when Original Document Source is From Another Node):
={{ $fromAI('Original_Source_Node_Name', `Exact name of the workflow node that produced the baseline (before) .docx binary (string, case-sensitive). Must match the node label exactly.`, 'string') }}Modified Source Node Name (when Modified Document Source is From Another Node):
={{ $fromAI('Modified_Source_Node_Name', `Exact name of the workflow node that produced the modified (after) .docx binary (string, case-sensitive). Must match the node label exactly. Must reference a different node from the original source — otherwise the diff will be empty.`, 'string') }}Clean View:
={{ $fromAI('Clean_View', `Boolean. Set true (recommended default) to compare the Accept All clean view of both documents — diffs reflect final content as if all tracked changes were accepted. Set false to diff the raw CriticMarkup-projected text including pending change markers — useful for auditing tracked-change differences themselves.`, 'boolean', true) }}Diff Format:
={{ $fromAI('Diff_Format', `One of 'wordPatch', 'unified', or 'structuredChanges'. 'wordPatch' (default) returns an Adeu @@ Word Patch @@ sub-word text diff on the 'diff' field — best for reading and explaining what changed. 'unified' returns a Git-style unified text diff on 'diff'. 'structuredChanges' returns a JSON array of DocumentChange objects on the 'changes' field instead of 'diff' — use it when you intend to feed the difference straight into apply_edits rather than describe it.`, 'string', 'wordPatch') }}Finalize
Reasoning (fill this FIRST):
={{ $fromAI('Reasoning', `State your reasoning for finalizing the document now BEFORE choosing the sanitize mode and options: what state the document is in, why it is ready for distribution, and who it is going to (signer, counterparty, internal). Always write this field first. This text is captured for audit only and does not alter engine behavior. One to three sentences is enough.`, 'string', '') }}Source Node Name (when Document Source is From Another Node):
={{ $fromAI('Source_Node_Name', `Exact name of the workflow node that produced the .docx binary (string, case-sensitive). Must match the node label exactly.`, 'string', 'Read Binary File') }}Source Binary ID (when Document Source is From Another Node):
={{ $fromAI('Source_Binary_Id', `Optional string. If you are finalizing a document that has been consecutively edited during this loop, pass the 'redlinedBinaryId' from your last tool execution here. Leave blank to sanitize the original baseline file.`, 'string', '') }}Sanitize Mode:
={{ $fromAI('Sanitize_Mode', `One of 'baseline', 'full', or 'keep-markup'. 'full' (recommended for distribution) strips author metadata, RSIDs, paragraph IDs, and proof errors AND requires all tracked changes to be resolved — pair with Accept_All=true to auto-accept. 'keep-markup' strips metadata but preserves visible tracked changes and comments — use when sending markup for counterparty review; pair with Author_Override to rewrite author names. 'baseline' is minimal cleanup only (RSIDs and proof errors) — leaves tracked changes and metadata intact.`, 'string', 'full') }}Accept All Tracked Changes (only meaningful when Sanitize Mode is full):
={{ $fromAI('Accept_All', `Boolean. Only applies when Sanitize_Mode is 'full'. Set true to auto-accept all pending tracked changes before sanitization. Set false (default) to block finalization and raise an error if any pending tracked changes exist, forcing them to be resolved explicitly. If multiple distinct authors are detected in pending changes when true, the report will include a warning about potential silent smuggles.`, 'boolean', false) }}Author Override (only meaningful when Sanitize Mode is keep-markup):
={{ $fromAI('Author_Override', `Optional string. Only applies when Sanitize_Mode is 'keep-markup'. When set, replaces the author name on every preserved tracked change and comment with this value (e.g. 'Acme Legal'). Leave empty to keep original authors intact.`, 'string', '') }}Protection Mode:
={{ $fromAI('Protection_Mode', `One of 'none' or 'read_only'. 'none' (default) leaves the document unlocked. 'read_only' injects a native Word read-only enforcement flag into settings.xml — Word users see a read-only banner and cannot edit without explicitly unlocking. Use 'read_only' for distribution to signers or counterparties when you want to discourage casual edits.`, 'string', 'none') }}Tip: The default-value (4th) argument of
$fromAI()lets the LLM omit the parameter entirely and fall back to a sensible default. Use defaults aggressively on optional fields so the LLM only has to specify what actually varies per call.
📂 Example Workflows
Two importable workflows ship in examples/. Neither can be run outside n8n: download the JSON, then in n8n use Workflows → Import from File.
Sequential_workflow.json — deterministic two-turn redline.
Manual Trigger → Read/Write Files (read) → Adeu Extract Markdown (Clean View on) → Code (build Gemini payload) → HTTP Request → Code (parse the DocumentChange array) → Adeu Apply Edits (Edits Source: From Input JSON) → Adeu Extract Markdown (raw view, reading from the previous Adeu node by name) → Code → HTTP Request → Code → Adeu Apply Edits (accept/reply actions) → Read/Write Files (write). No AI Agent, no tool nodes — this is the pattern to copy when you want a repeatable pipeline instead of an agent. Before running: set the two file paths and your Gemini API key and model id.
AI_Agent_workflow.json — conversational agent with three tools.
Chat Trigger → Read/Write Files (read) → Set (Restore Chat Context, because the file-read node drops chatInput/sessionId) → AI Agent, with extract_outline, extract_markdown and apply_edits on the ai_tool port and a Gemini chat model plus a buffer-window memory attached. After the Agent: Adeu Hydrate Tool Output → If ({{ $json.hydrated }}) → Read/Write Files (write). Before running: set the input file path, the output directory in Output Path Template, and pick your Gemini credential and model.
What the examples deliberately do not cover: Apply Text Revision, Generate Diff, and Finalize. Their $fromAI recipes above are complete; wire them the same way as the three tools in the agent example (Document Source: From Another Node, descriptionType: Set Manually).
Node names are load-bearing. The agent example's tools resolve the document with Source_Node_Name: 'Read Binary File'. Rename that node on the canvas and every tool call fails until you update the $fromAI default and the system message. The tool names the model sees are the canvas node names themselves.
🛠️ Error Handling & Troubleshooting
Because Adeu enforces Atomic Batch Validation by default, any error in the LLM's JSON will throw a NodeApiError and halt the node. The error message will tell you exactly which edit failed and why. (With Allow Partial Application enabled the node does not halt: the valid edits are saved, status reads "partial", and the same messages arrive in stats.failed keyed by 0-based edit index — the causes below read the same either way.)
- "Target text not found": The LLM hallucinated a word, altered the spacing, or the text doesn't exist in the baseline document.
- "Ambiguous match": The LLM used a
target_text(like "the Company") that appears multiple times. The error details will show you the exact occurrences. Advise the LLM to either include more surrounding context (e.g., "the Company shall indemnify") or usematch_mode: "all"if the intent is to replace every occurrence. - "Modification targets an active insertion...": The LLM tried to
modifytext that another author is currently tracking. Adeu explicitly blocks this to maintain virtual DOM integrity and clean redline threading. You mustacceptorrejectthat prior change first. (Editing plain text that merely sits under another author's comment is allowed — the comment anchor survives the tracked change.) - "...would sweep through a comment range from another author...": A
match_mode: "all"bulk replacement crossed a colleague's comment range. Blind fan-outs are blocked to protect foreign annotations; target the commented text deliberately withmatch_mode: "strict"or"first", or scope the edit outside the comment. - "Read-only elements": The LLM tried to modify structural items like cross-references or footnotes.
- "Requested page N exceeds total_pages (M). Call extractOutline to discover the page count first.": The LLM requested a page beyond what the document has. Have it call
Extract Outlinefirst to discover the page count.
Tip: If you are running bulk processing workflows, you can enable n8n's "Continue On Fail" setting on the Apply Edits node. If the LLM generates a flawed batch, n8n will catch the error, output an { "error": "..." } JSON object for that specific document, and continue processing the rest of the files in your queue. If you would rather keep the good edits from a flawed batch than lose the document entirely, enable Allow Partial Application instead (see Salvage Mode above) — you then get a redlined file plus stats.failed rather than an error object.
