|
@@ -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:
|