Browse Source

docs: the form trigger

fszontagh 1 month ago
parent
commit
80315618f0
1 changed files with 156 additions and 0 deletions
  1. 156 0
      docs/nodes.md

+ 156 - 0
docs/nodes.md

@@ -269,6 +269,162 @@ Any node may set it, not only the last one to run, so adding a node to the end
 of a workflow cannot silently change what its API returns. If several set it,
 of a workflow cannot silently change what its API returns. If several set it,
 the last one wins and the runner logs that it happened.
 the last one wins and the runner logs that it happened.
 
 
+## Form Trigger
+
+`form-trigger` (category `triggers`) publishes an HTML form at a shareable URL.
+Submitting it starts the workflow - a form field lets a person do what a webhook
+otherwise needs a developer for.
+
+### Where it is served
+
+The form is served at `/webhook/{workflowId}`. If the node's `path` field is
+set, that path is appended and must match exactly, e.g.
+`/webhook/<id>/apply`; leave it empty to answer at the bare workflow URL. `GET`
+renders the form, `POST` submits it.
+
+**The workflow must be published and active.** The webhook route only ever
+runs the published version - not whatever is currently open in the editor -
+and it refuses to answer at all if the workflow's `active` flag is off (a
+plain `400 Workflow is not active`). A form that was saved but never
+published, or was unpublished after being turned off, answers nothing useful.
+This is the single most likely reason a form "doesn't work": open the
+workflow and confirm both Publish and Active.
+
+### Fields
+
+Each entry in the node's `fields` array is one question:
+
+| Key | Meaning |
+|-----|---------|
+| `name` | The key the workflow reads the answer under. Letters, digits and underscore, not starting with a digit. |
+| `label` | Shown next to the input. |
+| `type` | One of `text`, `textarea`, `number`, `select`, `checkbox`, `date`, `file`. |
+| `required` | Blocks submission (with the form re-rendered and an error) if left empty. |
+| `placeholder` | Placeholder text, for the text-like types. |
+| `options` | The choices, for a `select` field. |
+| `accept` | For a `file` field, e.g. `image/*`. This is a browser hint only, sent as the `accept` attribute on the `<input>` - it is **not enforced server-side**. Nothing stops a submission with a different file type arriving anyway. A form that must reject a wrong file type needs a node downstream that inspects the actual bytes, not this field. |
+| `maxSizeMb` | For a `file` field. `0` uses the server-wide limit; a nonzero value can only tighten that limit, never loosen it - a per-field maximum above the server limit is meaningless because the oversized request is already refused before this check runs. |
+
+### Reading the answers
+
+Answers land on the trigger node's output under `form`, keyed by field name:
+
+```
+{{ $node['Form'].form.email }}
+{{ $node['Form'].form.howMany }}
+```
+
+A `file` field arrives as a binary object, the same shape a download produces:
+
+```json
+{
+    "type": "binary",
+    "data": "<base64>",
+    "mimeType": "image/png",
+    "filename": "photo.png",
+    "size": 48213,
+    "checksum": "<sha256 hex>"
+}
+```
+
+`checksum` is the SHA-256 of the base64 text, not of the raw bytes - the same
+convention `http-request`'s download path uses for its own binary output, so
+the two are directly comparable and an uploaded file can be handed straight to
+any node that expects a downloaded one.
+
+### Response modes
+
+`responseMode` is `immediate` or `wait`:
+
+- **`immediate`** returns the thank-you page (`responseMessage`) right away and
+  lets the workflow run in the background. **Anything that takes more than
+  about 30 seconds must use this mode** - the alternative blocks the browser
+  and times out.
+- **`wait`** holds the browser connection open until the workflow finishes, so
+  a `respond-to-webhook` node downstream can shape the actual response (status,
+  headers, body) the way [Answering a Webhook](#answering-a-webhook) describes.
+  The wait is capped at 30 seconds; a run that has not finished by then answers
+  with a timeout rather than hanging the browser indefinitely.
+
+Immediate-mode submissions run on a small bounded worker pool inside the
+webserver rather than one thread per request, so a burst of traffic against a
+public form cannot exhaust it. The pool is `server.form_dispatch_threads`
+(default 4) and `server.form_dispatch_queue_capacity` (default 32) in
+`config/webserver.json` - unset in the shipped config, so a stock install runs
+on those defaults. When the queue is full, the form answers **503** with a
+plain "This form is busy right now. Please try again in a moment." page
+instead of a false thank-you, so a bounded burst is visible as a retry-able
+failure rather than a silently dropped submission.
+
+### Upload size
+
+Two limits apply, and a submission has to pass both:
+
+- `server.max_upload_mb` in `config/webserver.json` bounds every request body
+  the webserver accepts, uploads included - 32 MB by default. Anything larger
+  is refused before the form handler ever sees it.
+- The runner's own gRPC message-size limit, `max_message_size_mb` in
+  `config/runner.json`, bounds the same body again on its way from the
+  webserver to the runner as part of the execution payload - 64 MB by default,
+  deliberately set above the upload limit so it never re-imposes a lower cap
+  than the one already enforced at the HTTP layer.
+
+A field's own `maxSizeMb` (see Fields, above) can tighten either of these
+further but never raise them.
+
+### Password
+
+`password` is optional; leave it empty and anyone with the link can submit the
+form. Set it and a visitor must enter it once (checked with a constant-time
+comparison) before the form is shown; on success they get a signed, HttpOnly
+cookie scoped to that workflow's webhook path, good for one hour, so they are
+not asked again for the rest of the session.
+
+**State plainly what this is worth**: the password is stored as plain,
+readable text in the live workflow document. Anyone who can read the workflow
+- anyone with editor access to the project - can read it directly, no
+decryption involved. It is a gate against a link being forwarded somewhere it
+shouldn't go, not a secret and not a security boundary. Do not rely on it to
+keep a form private from someone who already has access to the workspace.
+
+### No captcha
+
+There is no captcha and no rate limiting on the form endpoint beyond the
+bounded worker pool described above. A public form can be submitted
+repeatedly by a script as easily as by a person. A form that cannot tolerate
+that should be given a password, at minimum, and treated as a mitigation
+rather than a fix.
+
+### Submissions are executions
+
+There is no separate submissions list. A submission is a workflow execution
+like any other and is visible wherever executions are - the workflow's
+execution history - with the form answers as the trigger node's output.
+
+### Two things to know before debugging one at 2am
+
+**The form endpoint reveals that a form exists at a URL, even to someone who
+doesn't know its password.** An unknown workflow id gets a `404`, a workflow
+that exists but has no form node falls through to a structurally different
+response, and a workflow with a form node behind a wrong password gets a
+`200` password prompt. These three cases are distinguishable, so a visitor can
+tell "there is a form here" from "there is nothing here" without ever seeing
+the password itself. This was a deliberate choice, not an oversight - closing
+it would mean reshaping how every webhook on the platform answers a request
+for an id it doesn't recognize, and workflow ids are random UUIDs, so there is
+nothing practical to enumerate against it in the first place.
+
+**A node with a config field literally named `password` - the form trigger
+included - permanently shows as having unpublished changes in the editor,**
+even immediately after publishing. The upstream database daemon encrypts
+fields named `password` when it writes a version-history snapshot, while a
+live read of the same document stays plaintext; the editor's unpublished-changes
+indicator compares those two, and an encrypted value never equals its own
+plaintext. This is pre-existing platform behaviour that affects any node
+config with a `password`-named field, not something specific to forms - it is
+safe to ignore the indicator on a form-trigger node once you've confirmed the
+publish itself succeeded.
+
 ## Multiple Inputs
 ## Multiple Inputs
 
 
 A node that joins two branches names its inputs:
 A node that joins two branches names its inputs: