---
title: "Web Actions · RUAL Documentation"
description: "A button or form runs a web_action in the page's blueprint, which answers with updates: replace a row, remove it, insert one, update a counter or redirect."
canonical: https://docs.rual.nl/web/actions
language: en
---

# Web Actions

A button with `data-action="delete"` posts to the page's own URL, the blueprint runs its [`web_action`](https://docs.rual.nl/block-types/web/web_action) named `delete`, and that flow answers with update blocks: replace a row, remove it, insert one, update a counter or redirect. Only those parts of the page change.

In short

- The action's name is its `event` field. It belongs to the page's blueprint.

- The action receives the form's fields on `form` and the row that was clicked on `key`.

- Updates apply in the order the blocks ran. A [`web_render`](https://docs.rual.nl/block-types/web/web_render) in an action answers with a whole page instead.

- An action with no update blocks leaves the page as it is.

## The action block receives what the button sent

A [`web_action`](https://docs.rual.nl/block-types/web/web_action) is reached only by a `POST` to the URL of a [`web_page`](https://docs.rual.nl/block-types/web/web_page) in the same blueprint, with the field `_action` set to its name. The script in the page sends that for you. Give each action a name of its own within its blueprint. The name is the block's `event` setting, set in the [code view](https://docs.rual.nl/engine/studio-code-view) like a page's URL: `flow delete_row = web_action with {event: "delete"} {`.

| Out-pin | Is |
| --- | --- |
| `form` | The fields the form or button sent. A repeated name, or a name ending in `[]`, is a list. `_action` and `_key` are taken out. See [Forms](https://docs.rual.nl/web/forms#what-arrives). |
| `key` | The `data-key` of the closest element around the button: which row was clicked. Empty when there is none. |
| `connection` | The request, for the `httpconnection` blocks such as cookies and headers. |
| `params` | The `:name` segments of the page's URL. |
| `query` | The page's query string. |
| `language` | The language of the page. |

## An action has gates of its own

- The scopes of the page it was posted to must admit the visitor.

- If the action has `scopes`, those must admit the visitor too.

- If it has none, the scopes of **every** [`web_page`](https://docs.rual.nl/block-types/web/web_page) in the blueprint must admit the visitor.

- A request from another website is refused, whatever the scopes say.

The third rule means a public page is never the way in to an action written for the admin page next to it. It also means that a public action in a blueprint that also has an admin page needs `scopes: ["*public"]` of its own, or the public cannot use it. [Escaping and Security](https://docs.rual.nl/web/security#who-may-run-an-action) says what the visitor sees when refused.

## Six blocks answer the browser

They apply in the order they ran. Anywhere inside the action's flow counts, including a function the action calls.

| Block | Does in the browser | Its pins |
| --- | --- | --- |
| [`web_replace`](https://docs.rual.nl/block-types/web/web_replace) | Swaps an element for new HTML. The usual answer to an edit. | `key` or `target`, `html` |
| [`web_remove`](https://docs.rual.nl/block-types/web/web_remove) | Removes an element after a short fade. The usual answer to a delete. | `key` or `target` |
| [`web_insert`](https://docs.rual.nl/block-types/web/web_insert) | Adds HTML at the end or start of an element, or before or after it. | `key` or `target`, `html`, `position` |
| [`web_update`](https://docs.rual.nl/block-types/web/web_update) | Gives an element new contents and keeps the element: a cart counter, a total. | `key` or `target`, `html` |
| [`web_errors`](https://docs.rual.nl/block-types/web/web_errors) | Shows messages next to the form's fields. | `errors`, `message`, `target` |
| [`web_redirect`](https://docs.rual.nl/block-types/web/web_redirect) | Sends the visitor to another page. | `url`, `status` |

### An update needs a key or a target

- `key` finds `[data-key="..."]`. This is the way to reach a row.

- `target` is a CSS selector, such as `#cart-count`, used when `key` is empty.

- **Neither one is an error that stops the flow.** The action fails, and the answer tells a blueprint developer why.

- An element that is not on the page is not an error. The browser logs `no element for update` in its console and carries on, and a [`web_remove`](https://docs.rual.nl/block-types/web/web_remove) stays silent.

- These blocks work only in a flow started by [`web_action`](https://docs.rual.nl/block-types/web/web_action), and fail anywhere else. In a simulation there is no browser, so they do nothing.

### web_replace swaps the element itself

The new markup is the element, outer tag included. When the old element has a `data-key` and the new markup is a single element without one, the browser keeps the old key on it. A row stays reachable after an edit, even if its template does not mention the key.

### web_update keeps the element and its attributes

The contents become `html`. It is HTML, also when you meant plain text: pass user input through a [`web_template`](https://docs.rual.nl/block-types/web/web_template) first, so it gets escaped. A number from a stored document is fine as it is.

### web_insert has four positions

| `position` | The new HTML lands |
| --- | --- |
| `append` (default) | Inside the element, as its last child. |
| `prepend` | Inside the element, as its first child. |
| `before` | Right before the element, as a sibling. |
| `after` | Right after the element, as a sibling. |

Any other position is an error. Give an inserted row a `data-key` through the `key` pin of [`web_template`](https://docs.rual.nl/block-types/web/web_template), so later actions can reach it.

### web_errors writes next to the fields

`errors` is an object of field name to message, and `message` is for the form as a whole. [Forms and Validation](https://docs.rual.nl/web/forms#server-errors) has the details.

### web_redirect accepts paths and http(s) URLs

- **Allowed:** a path on this site (`/thanks`, `?page=2`, `#top`) or an `http://` or `https://` URL.

- **Refused with an error that stops the flow:** `javascript:` and any other scheme, and `//host` or `/\host`, which are another host dressed as a path.

- **In an action** the browser navigates there. Without JavaScript it is a `303`.

- **On a page request** the answer is a real redirect, `302` unless the `status` pin says 301, 303, 307 or 308. This is the one update block that also works in a [`web_page`](https://docs.rual.nl/block-types/web/web_page) flow.

## Render a row again with web_template

To answer an edit, render the same row template with [`web_template`](https://docs.rual.nl/block-types/web/web_template), hand it the updated document and its key, and pass its `html` to [`web_replace`](https://docs.rual.nl/block-types/web/web_replace). For a stored row, the template is just `{{> product-row}}`.

- **Delete:** `web_action(delete)`, storage remove by `key`, `web_remove(key)`.

- **Edit:** `web_action(save)`, storage update, [`web_template`](https://docs.rual.nl/block-types/web/web_template) of the row with the updated document, `web_replace(key, html)`.

- **Add:** `web_action(add)`, storage create, [`web_template`](https://docs.rual.nl/block-types/web/web_template) of the new row with its guid as key, `web_insert(target "#products")`.

```
blueprint "Products admin"

flow page = web_page with {event: "admin/products", scopes: ["*loggedin"]} {
  web_render(page.connection, title = "Products", template = "{{> products-page}}")
}

flow delete_row = web_action with {event: "delete"} {
  remove@products(guid: delete_row.key)
  web_remove(key: delete_row.key)
}

flow save_row = web_action with {event: "save"} {
  let {name, price} = save_row.form
  updated = update@products(guid: save_row.key, mutations: mutations{name: name, number price: price})
  web_replace(
    key: save_row.key,
    html: web_template(data: updated.object, key: save_row.key, template = "{{> product-row}}")
  )
}

flow add_row = web_action with {event: "add"} {
  let {name, price} = add_row.form
  created = create@products(mutations{name: name, number price: price})
  web_insert(
    target = "#products",
    html: web_template(data: created.object, key: created.guid, template = "{{> product-row}}")
  )
}
```

[`web_template`](https://docs.rual.nl/block-types/web/web_template) is a data block with no flow pins. With `key`, the fragment's first element gets that `data-key`, and one that already has its own keeps it. Plain text is wrapped in a ` ` so it can still be reached. A template error gives an empty `html`, `success` false and the message on `error`.

## A web_render in an action answers with a whole page

For an order confirmation, or anything where most of the page changes, end the action in a [`web_render`](https://docs.rual.nl/block-types/web/web_render).

- **With JavaScript** the browser takes the page's title and body, and scrolls to the top. The ` ` stays as it was, and scripts in the new body do not run.

- **Without JavaScript** it is simply the page the visitor gets.

- Update blocks that ran before it apply first, and then the page replaces the body.

- Choose this over a redirect when the page shows a result you do not want to store under a URL. Choose [`web_redirect`](https://docs.rual.nl/block-types/web/web_redirect) when a reload should not send the form again.

## An action with no updates leaves the page alone

It answers an empty list, and the page stays as it is. That is the right answer for something like logging a click or a load action that found nothing. Without JavaScript the visitor is sent back to the page with a `303`.

## A failed action tells the visitor little and you everything

A flow error answers a short message and logs the reason. The browser writes it to the console, brings back a row that `data-optimistic` hid, and fires a `rual:web-error` event. If the form has a `[data-error-for="_form"]` element, the message appears there: your form's `data-msg-failed` text if it has one. For a blueprint developer the answer also holds the full reason. If the visitor's session has run out, the answer is a `401` and the page reloads, which leads to the login.

## Limits

- **HTML an action returns is inserted as markup only.** Its scripts do not run. Put behaviour in data attributes, or in a script the page already loads.

- **Actions take form fields and JSON.** Upload files through the [upload endpoints](https://docs.rual.nl/tutorials/file-upload-processing) and pass the result to the action.

- **An action belongs to the page's blueprint.** A page cannot run another blueprint's actions directly. Call a shared function from the action instead: update blocks inside it still answer the same request.

- **A blueprint with pages of different scopes** needs scopes on each action, as an action without them requires what every page requires.

## Next steps

- [Forms and Validation](https://docs.rual.nl/web/forms): What the browser checks, and how server errors reach the right field.

- [Markup Reference](https://docs.rual.nl/web/markup): Triggers, confirmations, loading text and optimistic removal.

- [Common Blueprint Patterns](https://docs.rual.nl/blueprints/common-patterns): Flows you will combine with web pages: validation, branching, calling functions.

## Frequently asked

**How do I update one row of a page without reloading it in RUAL?**

Give the button data-action="name" and add a web_action block with that name to the page's blueprint. The action runs only for visitors its page (and its own scopes) allow, receives the row's data-key and answers with update blocks: web_remove removes the row, web_replace swaps it for a row rendered again with web_template, web_insert adds a new one. Only that piece of HTML is sent back.

**What can a RUAL web action answer with?**

Updates, applied in order: web_replace swaps an element, web_remove removes one, web_insert adds HTML, web_update changes an element's contents, web_errors shows messages on a form and web_redirect sends the visitor elsewhere. A web_render in an action answers with a whole new page, and an action with no updates leaves the page as it is.
