# 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](https://form2wf.com/docs/access) 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.

| Property | Description |
| --- | --- |
| `id` | Required, unique. |
| `label` | Button text. |
| `type` | `webhook` (default) or `form`. |
| `style` | `primary`, `secondary` (default) or `danger`. |
| `confirmMessage` | Asks for confirmation first. |
| `webhookUrl`, `webhookMethod`, `webhookHeaders` | For `webhook` actions. The URL supports `{{row.x}}`; methods GET/POST/PUT/PATCH/DELETE. |
| `targetForm`, `params` | For `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](#replies)). 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

| Property | Values |
| --- | --- |
| `buttonAction` | `webhook` (default), `navigate`, `url`, `back` |
| `buttonStyle` | `primary` (default), `secondary`, `danger` |
| `buttonWebhookUrl`, `buttonWebhookMethod`, `buttonWebhookHeaders` | For `webhook`. URL supports templates. |
| `buttonTargetForm`, `buttonTargetParams` | For `navigate`: a form slug in your organization, and params to pass. |
| `buttonUrl` | For `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:

| Key | Effect |
| --- | --- |
| `redirectUrl` | Sends 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. |
| `refresh` | `true` 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. |
| `message` | Text 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](https://form2wf.com/docs/n8n-recipes#crud).

---
Source: https://form2wf.com/docs/data-pages — Form2Workflow documentation (Building forms)
