npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

n8n-nodes-rawform

v0.1.4

Published

n8n community nodes that render fully author-controlled, unsanitised HTML as multi-page web forms, driven by n8n's durable wait/resume.

Readme

n8n-nodes-rawform

CI npm

Two n8n community nodes — Raw Form Trigger and Raw Form Response — that serve fully author-controlled, unsanitised HTML as multi-page web forms, driven by n8n's native durable wait/resume so a workflow can park on a form page for seconds or days.

They replicate the mechanism of stock n8n's Form Trigger / Form nodes. The one product difference:

The page markup is a raw HTML string you supply — your own <head>, <style>, <script>, arbitrary <form>/<input>, any framework (SurveyJS, a React bundle, Tailwind via CDN, …). Stock n8n runs custom form HTML through an allow-list that strips form, input, button, select, textarea, script, style, head, html, body, so custom HTML there is decoration-only. Here nothing is stripped.


⚠️ Do not install on a multi-tenant n8n

Installing a node that executes workflow-author-supplied HTML/JS is only safe when every workflow author is fully trusted.

Every served page runs under a null-origin sandbox CSP (below). That stops the page abusing the viewer's n8n session — it does not stop the author's JavaScript making arbitrary outbound fetches: exfiltrating submitted form data to a third party, or beaconing / port-scanning from the n8n host's network position (significant if n8n sits inside a corporate perimeter).

Do not install n8n-nodes-rawform on a multi-tenant n8n where workflow authors are not fully trusted. On a single-tenant / personal / trusted-team instance it is no more dangerous than a Code node.


Install

Community Nodes UI (n8n Settings → Community Nodes → Install):

n8n-nodes-rawform

or from a shell on the n8n host / image:

npm i n8n-nodes-rawform

Restart n8n. Two nodes appear: Raw Form Trigger (a trigger) and Raw Form Response (an action, "Next Page" or "Ending").

Supported n8n

2.36.x (SUPPORTED_N8N_RANGE in nodes/shared/version.ts).

The GET-render step of the wait/resume mechanism rests on an internal n8n implementation detail (WebhookHelpers.executeWebhook()'s early-return-when-no-workflowData). It is not covered by any n8n public API contract, so the supported range is verified per-version by our E2E suite (e2e/) and widened only after a green run on the new version — never on a version number alone. If you run a newer n8n and it works, open an issue with your version and we'll add it to the matrix.


How a flow is built

Raw Form Trigger ──▶ (your workflow) ──▶ Raw Form Response (Next Page) ──▶ … ──▶ Raw Form Response (Ending)
   GET  renders page 1                      parks, GET renders the next page          renders the final page
   POST starts the run                      POST resumes the run                      and completes the run
  • The Trigger GET serves page 1; its POST starts the execution.
  • Each Next Page Response node parks the execution (putExecutionToWait) and, on resume, its GET renders that page and its POST resumes the run with the submitted data.
  • The Ending Response node renders a final page (or a redirect, or a binary download). Its resume GET both shows the page and completes the execution, so any nodes wired after it run — this works on the poll path and even if the visitor reopens the link days later.

Page transitions use a 303 redirect to the signed resume URL, sent in milliseconds before the wait — the browser then polls that URL (200 = next page ready, 409 = still running) until the flow advances. Nothing depends on a held HTTP connection surviving across the wait, so a reverse proxy with a short idle timeout in front of n8n is fine.

Output shape

Both nodes output one item per submission:

{
  "json": {
    "data": {                     // ← your fields live under json.data (not flat)
      "email": "[email protected]",
      "topics": ["ml", "hardware"] // repeated name= → array
    },
    "submittedAt": "2026-08-30T14:12:03.000+01:00", // workflow timezone by default
    "formMode": "production"       // or "test" for a manual run
  },
  "binary": {                      // present only if the form uploaded files
    "resume": { /* n8n binary data */ }
  }
}
  • Fields are nested under json.data (deviation from stock's flat shape — it avoids collisions with submittedAt / formMode).
  • A repeated name= becomes an array; a single one stays a string.
  • File <input type="file"> values become n8n binary properties on the item (named after the field; suffixed _2, _3, … for multiples).
  • submittedAt is ISO-8601 in the workflow timezone unless you turn off Use Workflow Timezone on the trigger (then UTC).

Auth

On the Trigger, Authentication:

  • None (default) — the webhook is as exposed as any n8n webhook.
  • Basic Auth — an httpBasicAuth credential; the browser gets a 401 + WWW-Authenticate until it sends valid credentials.

There is no n8n User Auth option — n8n's form-user OAuth path is not reachable from a community node. Put an authenticating proxy in front if you need SSO.

Sandbox CSP (always on, no opt-out)

Every page is served with:

Content-Security-Policy: sandbox allow-scripts allow-forms allow-modals allow-popups allow-downloads

This is a null origin: no cookies, and the page's JS cannot reach n8n's authenticated API as the viewer. There is deliberately no node-level opt-out (stock has N8N_INSECURE_DISABLE_FORM_HTML_SANDBOX at the instance level if an operator truly needs same-origin). Your HTML is not otherwise sanitised — your form is responsible for its own XSS posture within its own null origin.

⚠️ Client-side {{ }} template engines are not supported in v1

Your Page HTML / Ending HTML is always run through n8n's expression evaluator first — that is how {{ $json.data.name }} works. n8n resolves every {{ … }} in the document, so a page built with a template engine that also uses {{ }} — Vue, Angular, Handlebars, Mustache — is mangled or throws before it ever reaches the browser.

Use one of:

  • %%…%% placeholder substitution for the values the node injects (%%N8N_SUBMIT_URL%%, %%N8N_TEST_MODE%%) — these are replaced after the expression pass and never collide with {{ }}.
  • A JS-config framework that does not use {{ }} in markup — e.g. SurveyJS (schema is a JS object), a hand-written React bundle using JSX / h(), htmx, Alpine (x-text), etc.

There is deliberately no "don't evaluate expressions" toggle in v1.

Recommend: set Limit Wait Time on public / high-volume forms

With Limit Wait Time off, an abandoned form parks forever and the execution row stays waiting indefinitely (same as the stock Wait node). On a public or high-volume form that piles up.

Turn Limit Wait Time on (Next Page node) with a Max Wait — at the cap n8n disables the parked node and resumes the workflow, passing the previous page's item through the node's single output unchanged. Detect the abandoned journey downstream (e.g. an IF on submittedAt / a marker field) — there is no separate "timed out" output.


Worked examples

Each block is a complete workflow you can paste into n8n (Workflows → ⋯ → Import from File won't take inline JSON; use Import from URL or just recreate the three nodes). Node parameters are shown; wire them Trigger → Response(s) on the main connector.

(a) Plain two-page form

Page 1 collects a name, page 2 confirms, an ending thanks the visitor.

Raw Form Trigger — Page HTML:

<!doctype html><meta charset="utf-8"><title>Step 1</title>
<h1>Your details</h1>
<form>
  <label>Name <input name="name" required></label>
  <label>Email <input name="email" type="email" required></label>
  <button type="submit">Next</button>
</form>

Raw Form Response — Page Type: Next Page, Page HTML:

<!doctype html><meta charset="utf-8"><title>Step 2</title>
<h1>Anything to add?</h1>
<form>
  <label>Notes <textarea name="notes"></textarea></label>
  <button type="submit">Finish</button>
</form>

Raw Form Response — Page Type: Ending, Respond With: Custom HTML, Ending HTML:

<!doctype html><meta charset="utf-8"><title>Thanks</title>
<h1>Thanks, {{ $json.data.name }}</h1>
<p>We'll be in touch at {{ $json.data.email }}.</p>

A single <form> with no n8nRawForm reference is auto-wired: the runtime sets method/action, intercepts submit, and drives the poll for you.

(b) SurveyJS page — manual submit via window.n8nRawForm.submit

When your page has no plain <form> (a JS widget owns the UI), call the injected API yourself. The runtime always exposes:

window.n8nRawForm.submit(formElementOrFormDataOrPlainObject) // → Promise<void>

Raw Form Response — Next Page, Page HTML:

<!doctype html><meta charset="utf-8"><title>Survey</title>
<link href="https://unpkg.com/survey-core/survey-core.min.css" rel="stylesheet">
<div id="survey"></div>
<script src="https://unpkg.com/survey-core/survey.core.min.js"></script>
<script src="https://unpkg.com/survey-js-ui/survey-js-ui.min.js"></script>
<script>
  const survey = new Survey.Model({
    elements: [
      { type: 'rating', name: 'nps', title: 'How likely are you to recommend us?' },
      { type: 'comment', name: 'why', title: 'Why?' }
    ]
  })
  survey.onComplete.add((s) => {
    // hand the flat answers straight to n8n; resolves once the run has resumed
    window.n8nRawForm.submit(s.data)
  })
  document.addEventListener('DOMContentLoaded', () =>
    survey.render(document.getElementById('survey')))
</script>

The answers arrive as {{ $json.data.nps }}, {{ $json.data.why }}. window.n8nRawForm.testMode is true under a manual/test execution if you want to branch on it. External CDN scripts require the sandbox's allow-scripts (always on) and network egress from the browser.

(c) Branching with a Switch

Route to a different page 3 based on a page-2 answer.

Trigger ─▶ Response "Page 2" ─▶ Switch ─▶ Response "Page 3 — team"
                                      └──▶ Response "Page 3 — solo"
                                                     both ─▶ Response "Ending"

Switch (n8n core) — Mode: Expression, Number of Outputs: 2, output:

={{ $json.data.plan === "team" ? 0 : 1 }}

Each downstream Raw Form Response ("Next Page") renders its own HTML and waits; both feed one Ending node. The resume URL is the same for every hop of a given execution, so branching needs nothing special — just wire the graph.

(d) Days-long park with an emailed resume link

Send the visitor away and let them come back via a link in their inbox. The signed resume URL is {{ $execution.resumeUrl }} — valid for the life of the parked execution (turn on Limit Wait Time to bound it).

Trigger ─▶ Send Email ─▶ Raw Form Response "Continue later" (Next Page)

Send Email (n8n core) — Text:

Hi {{ $json.data.name }},

Pick up where you left off any time in the next 7 days:
{{ $execution.resumeUrl }}

Raw Form Response — Next Page, Limit Wait Time: on, Max Wait: {{ $now.plus(7, 'days') }}, Page HTML: a simple "we've emailed you a link to continue" holding page. When they click the link days later, this node's GET renders the real next page and the flow resumes exactly where it parked — through an n8n restart, too.


Development

This repo uses bun.

bun install
bun run typecheck
bun run test                  # vitest unit suite — use `bun run test`, not `bun test`
bun run build                 # tsc + copy the icon svgs → dist/
bun run test:e2e              # 1 dockerised n8n, regular/production, all scenarios
bun run test:e2e -- --matrix  # full grid: {regular,queue} × {production,manual}

bun test invokes bun's own test runner and reports spurious failures — the unit suite runs under vitest via bun run test.

The E2E harness (e2e/README.md) is the load-bearing check — it stands up a real dockerised n8n, loads the built dist/ as a custom extension, and drives the whole wait/resume mechanism over real HTTP, because that mechanism rests on n8n internals no unit test can reach. It needs Docker and the pinned image (docker pull docker.n8n.io/n8nio/n8n:2.36.8).

Licence

MIT