Form2Workflow

Building forms

Data pages, tables & actions

Load data from n8n when a form opens, show it in fields and tables, and act on it with buttons and row actions.

Onload webhook#

Set an Onload Webhook URL in Form Settings. Each time the form loads, form2wf calls it and hands the response to the widget.

n8n receives (POST)
{
  "formId": "3f1c…",
  "formSlug": "customer-list",
  "params": { "status": "active" },
  "endUserId": null,
  "endUser": null,
  "metadata": { "timestamp": "…", "userAgent": "…", "ip": "…", "origin": "https://your-site.com" }
}

params holds the page's URL query parameters plus any values passed by navigation (a row action, a navigate button or link, a menu item), so an edit form can look up its record. On a Verified or Protected form, endUser identifies who is signed in — use it to return only their data. With the GET method, params arrive as query-string values plus a _metadata JSON string.

In n8n, set the Webhook node's Respond option to Using 'Respond to Webhook' node and reply with JSON:

n8n responds
{ "data": { "user": { "name": "Ada", "plan": "Pro" }, "orders": [ { "number": "A-1001", "total": "$40" } ] } }

The widget uses data (or the whole response if there is no data key). Fields read it with {{data.user.name}}; tables read it by path. If the onload call fails the form still renders, without data.

Testing it: in the form's Settings, Call webhook (under the onload webhook) runs the workflow once with the URL, method and headers in the dialog, saved or not, plus any URL params you type (e.g. id=42). It shows the HTTP status, the time taken and exactly what the form will read as data, or why the call failed. The payload's metadata.test is true, so a workflow can skip side effects during tests. The reply also fills the field editor's data source and key suggestions and lets the builder draw charts with your real data.

Pre-filling a form#

config — edit profile, pre-filled from onload
{
  "fields": [
    { "id": "who",   "type": "display", "label": "Account", "displayValue": "{{data.user.email}}" },
    { "id": "name",  "type": "text", "name": "name",  "label": "Name",  "defaultValue": "{{data.user.name}}" },
    { "id": "phone", "type": "tel",  "name": "phone", "label": "Phone", "defaultValue": "{{data.user.phone}}" },
    { "id": "uid",   "type": "hidden", "name": "user_id", "defaultValue": "{{data.user.id}}" }
  ]
}

Tables#

dataSource is a plain dot path into the onload data — orders or account.orders, not {{data.orders}}. If the onload data is itself an array, the table uses it directly. Columns are { "field", "label" } pairs; field may be a dot path. Empty tables show “No data available”.

{
  "id": "tbl", "type": "table", "label": "Customers", "dataSource": "customers",
  "columns": [
    { "field": "name",          "label": "Name" },
    { "field": "email",         "label": "Email" },
    { "field": "company.name",  "label": "Company" }
  ],
  "tableSearch": true,
  "pageSize": 25
}

tableSearch: true adds a search box that filters rows by any column; pageSize shows that many rows at a time with previous/next buttons. Both work on the rows the onload webhook returned, so for very large lists, have the workflow return a sensible subset (for example, filtered by a {{params.q}} value).

Row actions#

Add rowActions to a table to put buttons on every row.

PropertyDescription
idRequired, unique.
labelButton text.
typewebhook (default) or form.
styleprimary, secondary (default) or danger.
confirmMessageAsks for confirmation first.
webhookUrl, webhookMethod, webhookHeadersFor webhook actions. The URL supports {{row.x}}; methods GET/POST/PUT/PATCH/DELETE.
targetForm, paramsFor form actions: slug of the form to open, and values to pass it.
rowActions
"rowActions": [
  { "id": "edit", "label": "Edit", "type": "form", "style": "primary",
    "targetForm": "edit-customer",
    "params": { "id": "{{row.id}}", "name": "{{row.name}}", "email": "{{row.email}}" } },
  { "id": "delete", "label": "Delete", "type": "webhook", "style": "danger",
    "confirmMessage": "Delete this customer?",
    "webhookUrl": "https://n8n.example.com/webhook/customers/delete?id={{row.id}}",
    "webhookMethod": "DELETE" }
]

A webhook action sends the raw row object as the body, then redraws the form in place so the table shows fresh data (unless the reply redirects or navigates). A form action opens the target form in place; its fields can use the passed values as {{params.id}}, {{params.name}} and so on, and its onload webhook receives them in params — so it can load the full record by id instead of you passing every value.

Buttons#

PropertyValues
buttonActionwebhook (default), navigate, url, back
buttonStyleprimary (default), secondary, danger
buttonWebhookUrl, buttonWebhookMethod, buttonWebhookHeadersFor webhook. URL supports templates.
buttonTargetForm, buttonTargetParamsFor navigate: a form slug in your organization, and params to pass.
buttonUrlFor url: a web page opened in a new tab. Supports templates; only http(s), mailto and tel.

A webhook button sends { …onload data, …current input values } as the body and shows the response's data (text, or JSON) as a brief notification — or follows the reply's instructions, below.

{ "id": "resend", "type": "button", "label": "Resend invoice", "buttonAction": "webhook", "buttonStyle": "secondary",
  "buttonWebhookUrl": "https://n8n.example.com/webhook/invoices/resend", "buttonWebhookMethod": "POST",
  "buttonWebhookHeaders": [ { "key": "X-Api-Secret", "value": "…" } ] }

Replies that redirect, navigate or refresh#

A workflow behind a submit, a button or a row action can tell the page what to do next by adding these keys at the top level of its JSON reply:

KeyEffect
redirectUrlSends the visitor to that https page — for example a Stripe Checkout URL the workflow just created.
navigate{ "form": "slug", "params": { … } } opens another of your forms in place, like a navigate link.
refreshtrue redraws the form with fresh onload data. After a submit, the form shows again (handy for an "add" form above a table) instead of the thank-you message.
messageText for the visitor: the success message after a submit, or a notification after a button or row action.
n8n replies to a submit: take the visitor to payment
{ "redirectUrl": "https://checkout.stripe.com/c/pay/cs_live_…", "message": "Taking you to payment…" }
n8n replies to an approve button
{ "message": "Approved", "refresh": true }
n8n replies to a quote form: show the quote
{ "navigate": { "form": "quote-result", "params": { "quoteId": "Q-1042" } } }

Complete example: customer admin#

Two forms and three small n8n workflows give you a list / edit / delete admin page.

form: customers (no submit webhook, onload → list-customers)
{
  "showTitle": true,
  "fields": [
    { "id": "t", "type": "table", "label": "Customers", "dataSource": "customers",
      "columns": [ { "field": "name", "label": "Name" }, { "field": "email", "label": "Email" } ],
      "rowActions": [
        { "id": "edit", "label": "Edit", "type": "form", "targetForm": "edit-customer", "style": "primary",
          "params": { "id": "{{row.id}}", "name": "{{row.name}}", "email": "{{row.email}}" } },
        { "id": "del", "label": "Delete", "type": "webhook", "style": "danger", "confirmMessage": "Delete?",
          "webhookUrl": "https://n8n.example.com/webhook/customers/delete?id={{row.id}}", "webhookMethod": "POST" }
      ] }
  ]
}
form: edit-customer (submit webhook → update-customer)
{
  "fields": [
    { "id": "id",    "type": "hidden", "name": "id", "defaultValue": "{{params.id}}" },
    { "id": "name",  "type": "text",  "name": "name",  "label": "Name",  "defaultValue": "{{params.name}}", "required": true },
    { "id": "email", "type": "email", "name": "email", "label": "Email", "defaultValue": "{{params.email}}", "required": true },
    { "id": "back",  "type": "link", "label": "Back to list", "linkAction": "back" }
  ]
}

The n8n side of this example is in n8n recipes.