n8n-nodes-lenz 0.4.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,6 +19,15 @@ This is an n8n community node. It lets you use **Lenz** in your n8n workflows.
19
19
 
20
20
  Follow the [installation guide](https://docs.n8n.io/integrations/community-nodes/installation/) in the n8n community nodes documentation, and search for `n8n-nodes-lenz` under **Settings → Community Nodes → Install**.
21
21
 
22
+ ### Updating
23
+
24
+ **An installed copy does not update itself.** A fix only reaches your workflows once you update the node: **Settings → Community Nodes**, then **Update** on `n8n-nodes-lenz`. Worth doing if you installed it a while ago — an instance still on an early release keeps behaviour that has since been fixed. Before 0.1.10 in particular, a slow Verify gives up at 120 seconds with no way to collect the result afterwards: Get Verify Status arrived in 0.1.10, and Max Wait in 0.5.0.
25
+
26
+ Updating keeps your existing Lenz nodes on the node version they were added with, so saved workflows keep working unchanged. Two things do change underneath them, both deliberately:
27
+
28
+ - **Max Wait (Seconds) defaults to 300 from 0.6.0** (it was 120). A Verify node that never set Max Wait picks this up, because n8n does not save a parameter left at its default. It only ever makes a slow verification *finish* rather than time out; a fast one returns exactly as before. The limit is **per item**, and items are verified one after another, so the worst case for a run is the number of items times Max Wait — ten items can now block for up to fifty minutes where they used to give up after twenty. If a workflow must not block that long, or runs under a short execution timeout, set Max Wait explicitly.
29
+ - A node added on an old release keeps that release's layout. A node added **before 0.1.10 uses node version 1**, the original flat operation list, and stays on it after updating. Workflows built on it keep working. For anything new — templates especially — add a fresh Lenz node after updating so it uses the current layout.
30
+
22
31
  ## Operations
23
32
 
24
33
  Operations are grouped under a **Resource** picker. (Nodes added before v0.1.10 stay on node version 1, which shows the original flat operation list — existing workflows are unaffected.)
@@ -28,15 +37,15 @@ Operations are grouped under a **Resource** picker. (Nodes added before v0.1.10
28
37
  | Operation | What it does |
29
38
  |---|---|
30
39
  | **Verify (Deep)** *(default)* | Full multi-model pipeline (research → debate → adjudication), ~90 seconds. Returns a verdict, confidence, `lenz_score` (1-10), `key_finding`, sourced citations, and an executive summary. Reserve for high-stakes claims that need a thorough, cited answer. **Depth** trades work for price: *Low* searches fewer sources, skips the recovery fetch tiers and stops the debate after the opening arguments, for **5 credits instead of 10**. |
31
- | **Assess (Fast)** | A quick 3-model panel verdict, ~10 seconds, one entry per claim identified in the input text. Good default for lower-stakes checks. |
32
- | **Extract Claims** | Free — pulls the verifiable factual claims out of a block of text without checking them. Useful as a first step before running Assess or Verify on each claim individually. **Focus** narrows the result to the claims you describe (300 characters, no extra cost); when none of them match, `status` comes back as `no_match` with an empty list rather than the unfocused claims. |
40
+ | **Assess (Fast)** | A quick 3-model panel verdict, ~10 seconds, one entry per claim identified in the input text. Good default for lower-stakes checks. Each entry carries a `verification_url`, **which is usually empty, and that is expected**: it is set only when the claim was already deep-checked with **Verify (Deep)** and that result is one you can read. A fresh panel verdict creates no stored verification, so there is no page to link to. If you need a link and sources for a claim, run Verify (Deep) on it. |
41
+ | **Extract Claims** | Free — pulls the verifiable factual claims out of a block of text without checking them. Useful as a first step before running Assess or Verify on each claim individually. **Focus** narrows the result to the claims you describe (300 characters, no extra cost); when none of them match, `status` comes back as `no_match` with an empty list rather than the unfocused claims. **Text** can also be a single public web page URL: Lenz reads the page, or a YouTube video's transcript, and extracts the claims from its first 50,000 characters. Pages behind a login (Facebook, Instagram, Threads, LinkedIn) can't be read, and a URL call typically takes 5-40 seconds. |
33
42
 
34
43
  ### Verification — manage submitted and stored work
35
44
 
36
45
  | Operation | What it does |
37
46
  |---|---|
38
47
  | **Get Status** | Polls a submitted verification by `task_id`. Pairs with Verify's **Wait for Completion** toggle and with webhook delivery. |
39
- | **Select Claims** | Resolves a paused verification (see [Ambiguous and multi-claim input](#ambiguous-and-multi-claim-input)). |
48
+ | **Select Claims** | Resolves a paused verification (see [Multi-claim input](#multi-claim-input)). |
40
49
  | **Submit Batch** | Submits up to 20 claims at once without waiting. Returns one item per spawned task. Each claim can override the batch **Depth**, so one batch can mix 5- and 10-credit checks. |
41
50
  | **Get** | Retrieves a stored verification report by `verification_id`. |
42
51
  | **Get Many** | Lists the verifications stored against this API key, with **Return All** / **Limit**. |
@@ -65,9 +74,11 @@ Verify and Get also expose an **Include Audit Trail** toggle, which adds the adj
65
74
 
66
75
  ### Retry safety
67
76
 
68
- Billable calls (Verify, Assess, Extract, Submit Batch, Select Claims) send an `Idempotency-Key` derived from the execution ID, a hash of the node's identity, the item index, and a fingerprint of the request body. If n8n retries the node — via **Retry On Fail**, or after a dropped response — the input is identical, so Lenz replays the original response instead of charging you a second time. A fresh run of the workflow is a new execution, so it bills normally.
77
+ Billable calls (Verify, Assess, Extract, Submit Batch, Select Claims, Ask Follow-Up) send an `Idempotency-Key` derived from the execution ID, a hash of the node's identity, the item index, and a fingerprint of the request — its path as well as its body. If n8n retries the node — via **Retry On Fail**, or after a dropped response — the input is identical, so Lenz replays the original response instead of charging you a second time. A fresh run of the workflow is a new execution, so it bills normally.
69
78
 
70
- Including the body in the key is what makes repeated runs safe: **Loop Over Items** and **AI Agent** tool calls both execute the node several times within a single execution, each time restarting the item index at 0, so a position-only key would send one key with different inputs and the API would reject it.
79
+ On **Ask Follow-Up** the key also keeps the conversation clean: an unkeyed retry asks the question again, so the question and a second answer are appended to the history that **Get Ask History** returns and that the next follow-up reads as context. A retry that arrives while the first question is still being answered gets a `409` — there is no answer yet to replay.
80
+
81
+ Including the request in the key is what makes repeated runs safe: **Loop Over Items** and **AI Agent** tool calls both execute the node several times within a single execution, each time restarting the item index at 0, so a position-only key would send one key with different inputs and the API would reject it. The path is part of it because **Ask Follow-Up** and **Select Claims** name what they act on in the URL rather than in the body — asking one fixed question of several verifications, the wiring suggested below, is otherwise indistinguishable from a retry of the first.
71
82
 
72
83
  Note that **Assess bills per claim found in the text**, not per request: a paragraph containing five claims spends five assess units.
73
84
 
@@ -85,6 +96,7 @@ Built against `n8n-workflow` (n8n API version 1) and tested against n8n v2.30.4.
85
96
  ## Usage
86
97
 
87
98
  - **Verify (Deep) takes ~90 seconds** — it's the full multi-model pipeline, not an instant call. The node blocks/polls until the result is ready, so no separate polling setup is needed on your end.
99
+ - **If it outlasts Max Wait (Seconds)**, the node returns `status: "timeout"` with the `task_id` and `passed: null` instead of a verdict. Nothing is lost — giving up does not cancel anything, the verification keeps running server-side, and the credits were spent at submit either way. Collect it later with **Get Verify Status**, or raise **Max Wait (Seconds)** (default 300 — it was 120 before 0.6.0, which a standard-depth run can outlast; see [Updating](#updating)). Max Wait is a ceiling, not a delay: a verification that finishes sooner returns as soon as it does. A poll that fails with a 5xx, a 429 or a 408/425 is retried for as long as the window lasts rather than failing the item; a 429 waits the reopening time the API states instead of returning on the node's own cadence. A failure carrying no HTTP status at all — a DNS or TLS problem, say — is different: it could equally be a bug or a bad credential, so it gets two retries and is then surfaced as the error it is. If the window ends on a failed poll, the `timeout` result names it in `last_error` and `last_error_status`.
88
100
  - **Low depth drops a debate round, not just sources.** As well as searching fewer sources and skipping the recovery fetch tiers, *Low* stops the debate after the opening arguments — so with **Include Audit Trail** on, `audit.debate_pro.rebuttal` and `audit.debate_con.rebuttal` come back as empty strings. Every step that does run uses the same models, so Low is a volume lever rather than a model downgrade — but it is one round of argument fewer, not merely a narrower search.
89
101
  - **You are charged for the Depth you asked for, not the one you got.** A *Low* request that Lenz can answer from an existing *standard* verdict still costs 5 — and the `depth` field on the result reads `standard`, because it describes the evidence behind the verdict rather than the request. Seeing `standard` come back from a *Low* request is correct, not a bug.
90
102
  - To feed data from a previous node instead of a fixed value, toggle a field to **Expression** and reference it, e.g. `{{ $json.output }}`.
@@ -106,8 +118,11 @@ A simple "fact-check gate" pattern — verify an LLM's output before acting on i
106
118
 
107
119
  1. Add an **LLM node** (or any node producing text) upstream.
108
120
  2. Add the **Lenz node**, set Operation to **Verify (Deep)**, and set the Claim field to an expression referencing the upstream output, e.g. `{{ $json.text }}`.
109
- 3. Add an **IF node** after Lenz with the condition `{{ $json.passed }}` **is true**.
110
- 4. Wire the true branch to continue the workflow normally, and the false branch to whatever your "needs review" path is (Slack alert, email, a manual-approval step, etc.).
121
+ 3. Add an **IF node** after Lenz with the condition `{{ $json.status }}` **equals** `completed`. Send the false branch wherever unfinished work should go — a timeout, a `failed` pipeline or a `needs_input` interrupt is not a verdict, and its `passed` is `null`.
122
+ 4. After that, add a second **IF node** with the condition `{{ $json.passed }}` **is true**.
123
+ 5. Wire its true branch to continue the workflow normally, and its false branch to whatever your "needs review" path is (Slack alert, email, a manual-approval step, etc.).
124
+
125
+ > **Check `status` before `passed`.** `passed` only means something once a verdict exists. A timeout, a `failed` pipeline or a `needs_input` interrupt has no verdict, and `passed` is `null` or absent — which an IF reads as false. Branching straight on `passed` therefore reports "this claim did not pass" for a claim nobody ever checked, and a provider outage becomes a debunking.
111
126
 
112
127
  For a lighter check on lower-stakes content, swap the Lenz operation to **Assess (Fast)** instead — same wiring, ~10s instead of ~90s.
113
128
 
@@ -128,15 +143,13 @@ Ask a grounded question about the evidence behind a Verify (Deep) result, by cha
128
143
  3. Set the Verification ID field to an expression referencing the first node's output: `{{ $json.verification_id }}`.
129
144
  4. Keep the Question field as a fixed string (e.g. `"What are the main sources supporting this verdict?"`) — it works for whatever claim was just verified, since only the Verification ID needs to change per run.
130
145
 
131
- ### Ambiguous and multi-claim input
146
+ ### Multi-claim input
132
147
 
133
- Verify pauses rather than guessing when the text isn't a single unambiguous claim. The result comes back with `status: "needs_input"` and a `reason`:
148
+ Verify pauses rather than guessing when the text contains several distinct claims. The result comes back with `status: "needs_input"` and a `reason`:
134
149
 
135
150
  | `reason` | What the node returns | How to continue |
136
151
  |---|---|---|
137
152
  | `multi_claim` | `claims` — the distinct claims found in your text | Feed the ones you want into **Select Claims** with the same `task_id` |
138
- | `clarification_required` | `candidates` — the possible readings of one ambiguous claim | Feed the intended reading into **Select Claims** with the same `task_id` |
139
- | `duplicate_found` | `similar_claims` — existing verifications that already cover this | Reuse one of those `verification_id`s, or rephrase to force a fresh check |
140
153
 
141
154
  **Select Claims** spawns one independent verification per selected claim and returns one item each, so you can poll them with **Get Status** or collect them via webhook:
142
155
 
@@ -149,7 +162,7 @@ Verify pauses rather than guessing when the text isn't a single unambiguous clai
149
162
  {{ $json.claims[0].text }}
150
163
  ```
151
164
 
152
- A paused task expires **10 minutes** after it pauses, and Select Claims only accepts text that was actually offered — so copy the claim text verbatim rather than retyping it.
165
+ A paused task stays open for **24 hours from submission** (not 10 minutes, as earlier versions of this README said — resubmitting inside that window pays again for a check Select Claims would still have completed), and Select Claims only accepts text that was actually offered — so copy the claim text verbatim rather than retyping it.
153
166
 
154
167
  ### When a verification fails
155
168
 
@@ -165,23 +178,39 @@ Both `failure_class` and `retryable` are empty/`null` on verifications older tha
165
178
 
166
179
  Separately, a *submit* can be refused outright with **HTTP 503** and a typed body code — `capacity` (Lenz is at its concurrency ceiling) or `upstream_unavailable` (model providers down). The node reports these as transient and names the stated wait (typically 90-120s, jittered so callers return spread out). Nothing is charged for a refused submit.
167
180
 
168
- The wait is longer than **Retry On Fail** can cover: that setting allows 2-5 tries spaced a few seconds apart, so it would spend every try inside the window and fail anyway — while re-sending the submit each time, which is the pile-on the jitter exists to prevent. Handle it in the workflow instead: set the node's **On Error** to *Continue (using error output)*, feed that output into a **Wait** node set to the stated seconds, and loop it back into the Lenz node. Re-running the workflow later works just as well.
181
+ The wait is longer than **Retry On Fail** can cover: that setting allows 2-5 tries spaced a few seconds apart, so it would spend every try inside the window and fail anyway — while re-sending the submit each time, which is the pile-on the jitter exists to prevent. Handle it in the workflow instead: set the node's **On Error** to *Continue (using error output)*, feed that output into a **Wait** node set to the stated seconds, and loop it back into the Lenz node. **Set the Wait node's Wait Unit to Seconds** — it defaults to Hours, so a 90 left on the default waits 90 hours. Re-running the workflow later works just as well.
169
182
 
170
183
  So the Wait node has a number to read, the error output carries the refusal as fields rather than only as prose:
171
184
 
172
185
  | Field | What it says |
173
186
  |---|---|
174
- | `retry_after` | Seconds to wait before submitting again — point the Wait node's duration at `{{ $json.retry_after }}` |
175
- | `code` | The typed reason, e.g. `capacity`, `upstream_unavailable`, `no_credits` |
176
- | `status_code` | The HTTP status, e.g. `503` |
187
+ | `retry_after` | Seconds to wait before submitting again — safe to point a Wait node's duration at, **with its Wait Unit set to Seconds** (the default is Hours). Emitted only when the wait is short enough to be worth waiting (a 503, or a short rate limit); a long cap reset comes back as `resets_in_seconds` instead, and a 402 has nothing to wait for |
188
+ | `code` | The typed reason, e.g. `capacity`, `upstream_unavailable`, `rate_limited`, `no_credits` |
189
+ | `status_code` | The HTTP status, e.g. `429`, `503` |
177
190
  | `cost` | Credits the refused call needed, present only on an out-of-credits refusal |
178
191
  | `credits_remaining` | Credits the account holds — `0` is a real value and is reported, not dropped |
179
- | `error_message` / `error_description` | The same wording the node would have thrown, present only for a recognised billing or capacity refusal |
192
+ | `error_message` / `error_description` | The same wording the node would have thrown, present only for a recognised billing, capacity or rate-limit refusal |
193
+ | `resets_in_seconds` | Seconds until a rate limit clears, when that is too long to sit in a Wait node — see [Rate limits](#rate-limits-http-429) |
194
+ | `limit` | The limit the API stated, on a Lenz rate-limit refusal |
195
+ | `upgrade_url` | Where that limit or plan is raised — on a Lenz rate-limit refusal, and on an out-of-credits refusal |
196
+
197
+ Each of these is emitted when the API's response carries it, so treat the "when" column as what today's API does rather than as a guarantee.
180
198
 
181
199
  `cost` and `credits_remaining` let an **IF** node tell a shortfall from an empty balance without reading the prose: `{{ $json.credits_remaining }}` above zero is one top-up away, zero is a plan decision.
182
200
 
183
201
  `error` keeps the raw message it always carried, so existing workflows reading it are unaffected.
184
202
 
203
+ ### Rate limits (HTTP 429)
204
+
205
+ **Extract Claims** is free and capped per account per day (resetting 00:00 UTC), so a 429 is the refusal a busy workflow is most likely to meet. Nothing is charged for a refused call. The body states `reset_in_seconds`, which the node reports under one of two names depending on how long it is — `retry_after` for a wait worth sitting through, `resets_in_seconds` for one that is not. The node's message says which you have:
206
+
207
+ - **A short reset** (roughly five minutes or less) arrives as `retry_after`, and is the Wait-node loop described above — error output into a **Wait** node with Wait Amount `{{ $json.retry_after }}` and **Wait Unit Seconds** (the default is Hours), looped back.
208
+ - **A longer reset** — anything over about five minutes — arrives as **`resets_in_seconds`**, and deliberately *not* as `retry_after`: waiting it out inside a workflow leaves the execution pending that long, where an execution timeout or a Cloud duration limit can cancel it before the limit clears. The daily cap is the common case: it resets at midnight UTC, so the wait can be tens of thousands of seconds. Re-run the workflow after the reset, schedule it for then, or raise the cap.
209
+
210
+ Splitting the two keys is what keeps `retry_after` meaning what this table says it means — a duration you can hand to a Wait node. A workflow already built on the documented pattern therefore keeps failing fast on a daily cap instead of silently parking for the rest of the day. To handle the long case, read `resets_in_seconds` explicitly.
211
+
212
+ Note that the wait is read from the response body, not from the `Retry-After` header. The API sends the header, but n8n wraps every failed request in a `NodeApiError` that keeps the parsed body and discards the response object, so by the time the node sees the error the header is gone.
213
+
185
214
  ## Resources
186
215
 
187
216
  * [n8n community nodes documentation](https://docs.n8n.io/integrations/#community-nodes)
@@ -224,6 +253,10 @@ So the Wait node has a number to read, the error output carries the refusal as f
224
253
 
225
254
  This release also adds a `pre-push` git hook that refuses to push agent commits which have not been reviewed. It only affects contributors, needs `git config core.hooksPath .githooks` once per clone, and never gates a human pushing by hand — see AGENTS.md.
226
255
 
256
+ * **0.5.0** — **Verify (Deep) no longer loses a verification you have already paid for.** Credits are debited the moment a submit is accepted, and the poll loop that waits for the verdict had no tolerance for a failed poll: one 502 or network blip on any of roughly fifteen polls threw straight out of the operation and took the `task_id` with it, leaving a verification that was charged for, still running server-side, and unreachable — not even **Get Verify Status** could fetch it back. Transient poll failures (5xx, 429, and the 408/425 an intervening proxy produces) are now retried for as long as the wait lasts, and a 429 waits the reopening time the API states rather than returning on the node's own cadence, which is what tripped the limiter to begin with. A failure carrying no HTTP status is treated differently on purpose — a DNS or TLS problem is indistinguishable from a bug or a bad credential, so it gets a small bounded number of attempts and is then surfaced as the error it is, rather than retried for the window and reported as a timeout, which would invent a fact. The `task_id` now rides every post-submit failure, on the error output and on the thrown error alike. The hardcoded 120-second deadline becomes **Max Wait (Seconds)**, validated and clamped before the claim is submitted — validated afterwards, a non-numeric expression got the claim charged and then threw an error with nowhere to carry the task id, which is the same bug by another route. Non-verdict terminals now carry `passed: null` so the key is visible in n8n's output schema; be aware that null is still falsy, so an **IF** node reading `{{ $json.passed }}` alone routes a timeout down the false arm exactly as before. Checking `status` first is the actual fix, and the README's gate pattern now leads with it — a provider outage reading as a debunking was the real bug. A timeout that ends on a failed poll names it in `last_error` and `last_error_status`, instead of reporting success-shaped JSON that implies the task was seen running. Alongside that, four correctness fixes a first external user would hit: **Get Many** duplicated and skipped rows above Limit 100 (#21), **Select Claims** collided two paused tasks that offered the same claim text onto one Idempotency-Key (#22), validation failures surfaced as API errors with no HTTP code (#23), and **Ask Follow-Up** sent no Idempotency-Key, so a retry appended a second question and answer to the stored conversation. No output field was removed or renamed.
257
+
258
+ * **0.6.0** — **Three changes reach a workflow you already have, as soon as you update.** First, **Max Wait (Seconds) defaults to 300, up from 120.** A real verification finished in Lenz with a verdict and the credits were taken while the node had already given up at 120 seconds and returned `timeout`; Lenz measures a standard-depth run at about 90 seconds median with the tail past 120, so this was not rare. Because n8n does not save a parameter left at its default, every Verify node that never set Max Wait picks up the new value. It is a ceiling, not a delay — a fast verification returns exactly as before — but it is per item, so a ten-item run can now block for up to fifty minutes instead of twenty; set Max Wait explicitly if a workflow runs under a short execution timeout. Second, `progress` loses `content` and `step_stats` (below). Third, the `clarification_required` and `duplicate_found` pauses are gone, since the API no longer returns them: `multi_claim` is the one `needs_input` reason left, `candidates` and `similar_claims` stay on the output but are always empty until their removal on 2026-11-29, and Assess reports an empty result as `no_claim` rather than `ambiguous`. No parameter was renamed or removed. **Rate limits (HTTP 429) now have proper handling.** They arrived as n8n's stock "Request failed with status code 429"; the message now names the limit, when it clears and that nothing was charged. The documented Wait-node recovery was being fed `undefined`, because the error output read a key a 429 does not carry — a short reset now arrives as `retry_after`, and one too long to sit in a workflow, like the `/extract` daily cap that clears at midnight UTC, arrives as `resets_in_seconds` so that a workflow already wired to the Wait loop fails fast instead of parking for most of a day. The advice also names the Wait unit, because n8n's Wait node defaults to Hours and a bare `{{ $json.retry_after }}` turned a 45-second limit into a 45-hour wait. A 429 from a proxy or CDN in front of Lenz is no longer reported as your plan being too small. And a rate-limited status poll no longer abandons a verification you have paid for: a daily-cap 429 on the first poll used to sleep the whole of Max Wait in one go and then report `timeout` for a verification that had finished minutes in. The README gains an **Updating** section and explains why `verification_url` on Assess is usually empty. The rest of this entry covers the `progress` change and the other poll-loop fixes. **What a mid-run poll hands your workflow is now a fixed list.** While a Verify (Deep) runs, **Get Verify Status** returns a `progress` object, and the node used to pass whatever the API put in it straight onto the item. That endpoint was inherited from the consumer progress page and nothing shaped it, so what came through included `content` — the accumulated evidence pool, with full untruncated source quotes and every panelist's reasoning — and `step_stats`, Lenz's own per-step cost in EUR. Neither was ever part of the contract. `progress` now carries `step`, `index`, `total`, `elapsed_seconds` and `poll_after_seconds`, and nothing else; anything the API adds in future stays out until it is added deliberately. **If you mapped `progress.content` or `progress.step_stats`, those fields are gone** — that is the change, not a side effect of it. A `processing` response that states `poll_after_seconds` is now honoured instead of the node's own 2/4/8s cadence, since the server knows which stage it is in and how long that stage runs; a value outside the sane range is ignored rather than clamped. Reviewing that change turned up a fault in the poll loop that predated it: the deadline was enforced by the loop condition, so a wait clamped to the deadline was followed by the loop ending rather than by one more read — a verification that finished during that last wait was reported as a timeout, sending you to fetch a verdict that had already arrived. The loop now always ends on a read. Separately, a correction that was costing money: a paused task stays open for **24 hours from submission**, not the 10 minutes the Selected Claims field and this README both claimed. Anyone who believed that resubmitted, and paid a second time, for a **Select Claims** that would still have worked.
259
+
227
260
  ## Maintainer
228
261
 
229
262
  [@David19782](https://github.com/David19782)