---
title: "Build Your First CRUD App · RUAL Documentation"
description: "Step-by-step RUAL tutorial: plan a schema-less task document, then create, list, update, delete, and search tasks with blueprint blocks, no code needed."
canonical: https://docs.rual.nl/getting-started/first-crud-app
language: en
---

# Build Your First CRUD App

A step-by-step tutorial: plan a schema-less task document, then create, list, update, delete, and search tasks with blueprints, no code required.

In this tutorial you build a small **Tasks** manager: a page that lists tasks, a form to create them, a detail view, and edit, delete, and search functionality. Everything runs on blueprints and the built-in storage. You will not write any backend code.

Before you start, make sure you have access to a cluster (see [Getting Access](https://docs.rual.nl/cluster/getting-access)) and that you know your way around the blueprint canvas (see [Blueprint Introduction](https://docs.rual.nl/blueprints/introduction)). Remember to hit `Activate` in the top bar of each blueprint, or your pages will not respond.

### 1. Plan Your Data

RUAL storage is JSON-based and schema-less. You don't create tables or declare fields up front. The tradeoff is that there are no relationships between documents, so each document carries everything you want to display (denormalization). A task document looks like this:

```
{
  "owner_guid": "a23575f49d0af385314c1f02280163374297e018a692e5e4ab85eb307ebf6ebc",
  "owner_name": "joe doe",
  "status": "open",
  "title": "write the release notes"
}
```

Three things to notice:

- **Everything is lowercase.** Field names are always stored in lowercase, and searches are case-sensitive: storing searchable values in lowercase saves you from missed matches later.

- **The owner is denormalized.** Instead of only referencing a user, we embed `owner_name` so the list page can show it without a second lookup. We keep `owner_guid` as the reference for when we do need the full user document.

- **There is no `created` field.** Creation and update timestamps live in `_meta` (`_meta.created`, `_meta.updated`). Never in the document root. Do not set your own copy of them.

Every document automatically gets a `_meta` object with its unique `guid`, timestamps, and removal state. You will see it in the output of the create step below. For the full storage reference. See [Storage](https://docs.rual.nl/blueprints/storage).

### 2. Create a Task

Create a new blueprint of type `Build a UI Page`. This seeds the canvas with a [`state_page`](https://docs.rual.nl/block-types/state%20ui/state_page) block. Build a small form with a [`state_form`](https://docs.rual.nl/block-types/state%20ui/state_form) containing a [`state_input_dynamic`](https://docs.rual.nl/block-types/state%20ui/state_input_dynamic) (field `title`) and a [`state_button`](https://docs.rual.nl/block-types/state%20ui/state_button). When the button is clicked, the create flow runs:

- Get the submitted values with [`state_form_get`](https://docs.rual.nl/block-types/state%20ui/state_form_get), which outputs the form data as an object.

- Add a [`storage`](https://docs.rual.nl/block-types/storage/storage) block and select your `tasks` storage.

- Add a [`mutations_set_bp_field_multiple`](https://docs.rual.nl/block-types/mutations/mutations_set_bp_field_multiple) block and select the fields to set: `title` from the form, `status` = `open`, and the owner fields from the current user. You never set a creation timestamp yourself: storage writes `_meta.created` for you.

- Add a [`function_create_document`](https://docs.rual.nl/block-types/storage/function_create_document_from_mutations) block. Connect the storage and mutations to its in-pins, and connect the flow pin from the button's click event.

The block returns the created document: including the generated `_meta` with its `guid`: on the `object` out-pin, and the raw guid on the `guid` out-pin:

```
{
  "_meta": {
    "cms": 1752768000362,
    "created": 1752768000,
    "entity": 1,
    "expiry": -1,
    "guid": "b34686f50e1b04964250f13391274d85308c129a793c6ec97bc41870fc67081e",
    "removed": 0,
    "ums": 1752768000803,
    "update_hash": "60e27209fa93063b5605e41605c2722ed428cae0",
    "updated": 1752768000
  },
  "owner_guid": "a23575f49d0af385314c1f02280163374297e018a692e5e4ab85eb307ebf6ebc",
  "owner_name": "joe doe",
  "status": "open",
  "title": "write the release notes"
}
```

### 3. Build the List Page

The list page is another UI page blueprint. Its flow searches the `tasks` storage and hands the results to a table. Build the search query first: queries are composed by connecting query blocks and feeding them into [`function_search`](https://docs.rual.nl/block-types/globals%2Cstorage/function_search):

| Block | Purpose | Connects To |
| --- | --- | --- |
| `storage` (tasks) | Selects the storage to search in | `function_search` (storage pin) |
| [`query_bool_term_fields`](https://docs.rual.nl/block-types/query/query_bool_term_fields) (status = open) | Match only open tasks | [`query_bool_filter`](https://docs.rual.nl/block-types/query/query_bool_filter) |
| [`query_bool_filter`](https://docs.rual.nl/block-types/query/query_bool_filter) | Wrap the term query in an efficient filter context | `query_and` |
| [`query_sort_field`](https://docs.rual.nl/block-types/query/query_sort_field) (_meta.created, desc) | Newest tasks first | `query_and` |
| `query_and` | Combine filter and sort into one query | `function_search` (query pin) |
| `number_default` (25) | Limit the result set | `function_search` (limit pin) |

`function_search` outputs an `array` of matching documents. Render them with an [`state_advanced_datatable`](https://docs.rual.nl/block-types/state%20ui/state_advanced_datatable): select the `tasks` storage on the block, pick the columns (`title`, `status`, `owner_name`), and place its `state` out-pin on the page. To render custom cells. A status badge, an edit button per row. Use the table's row component out-pin; see [Iterations](https://docs.rual.nl/interfaces/iterations) for how iterative content works.

### 4. Show Task Details

For the detail view, create a UI page with the task guid in its URL. The `state_page` block exposes the URL parameters on its `params` out-pin: take the `guid` from there and feed it into a [`function_get_document`](https://docs.rual.nl/block-types/storage/function_get_document) block with the `tasks` storage selected.

Getting by guid is the fastest retrieval method and easier to cache than searching. The block continues on the `found` flow pin when the document exists, so render the fields there and show a "task not found" state on the other path. To load several tasks at once: for example all tasks of a project. Use [`function_get_documents`](https://docs.rual.nl/block-types/storage/function_get_documents) with an array of guids.

### 5. Update and Delete

Updates go through mutations, RUAL's transaction-based update system: each update is processed sequentially, so two users editing the same task can't overwrite each other. To mark a task as done, use [`function_update_document`](https://docs.rual.nl/block-types/storage/function_update_document_mutations) with the task's `guid` and a [`mutations_set_bp_field_multiple`](https://docs.rual.nl/block-types/mutations/mutations_set_bp_field_multiple) that sets `status` to `done`. The same mutation blocks you used at creation work here: increments, array adds, field removals; see [Updating Documents](https://docs.rual.nl/blueprints/storage#updating-documents) for the full list.

For deletion you have two options:

- [`function_remove_document`](https://docs.rual.nl/block-types/storage/function_remove_document), **soft delete**. The document is marked as removed and excluded from search results, but the data stays and can be brought back with [`function_restore_document`](https://docs.rual.nl/block-types/storage/function_restore_document).

- [`function_delete_document`](https://docs.rual.nl/block-types/storage/function_delete_document), **permanent delete**. The document is gone for good (unless revisions are enabled on the storage).

### 6. Add Search

Finally, let users find tasks by title. Add a [`state_input_dynamic`](https://docs.rual.nl/block-types/state%20ui/state_input_dynamic) (field `q`) above the list and trigger the search flow on enter or on a button click. The flow builds a full-text query and runs the same `function_search` as before:

| Block | Purpose | Connects To |
| --- | --- | --- |
| [`query_bool_simple_query_string_field`](https://docs.rual.nl/block-types/query/query_bool_simple_query_string_field) (field: title) | Full-text match of the user's input on `title` | `query_and` |
| [`query_bool_filter`](https://docs.rual.nl/block-types/query/query_bool_filter) (status = open) | Keep the open-tasks filter from step 3 | `query_and` |
| [`query_sort_field`](https://docs.rual.nl/block-types/query/query_sort_field) (_meta.created, desc) | Keep the sorting | `query_and` |
| `query_and` | Combine text query, filter, and sort | `function_search` (query pin) |

[`query_bool_simple_query_string_field`](https://docs.rual.nl/block-types/query/query_bool_simple_query_string_field) handles common user search syntax, which makes it the right choice for user-facing inputs. For autocomplete-style or typo-tolerant search. See the other text query blocks in [Full-Text Search](https://docs.rual.nl/blueprints/storage#full-text-search).

### 7. Next Steps

You now have a working CRUD app: documents created with mutations, listed with a composed query, read by guid, updated transactionally, and soft-deleted. From here:

### Frequently asked

**Where does RUAL store the creation date of a document?**

In the _meta object, as _meta.created, together with _meta.updated, _meta.guid and the removal state. Storage writes those fields for you, so you never add your own created field at the root of the document.

**Does RUAL storage support relationships between documents?**

No. Storage is JSON-based and schema-less, and there are no relationships between documents. Each document carries the values it needs to be displayed, which is why examples embed a name next to the referenced guid.

**Why are my RUAL storage searches not finding anything?**

Field names are always stored in lowercase and searches are case-sensitive. Store searchable values in lowercase when you write them, so the query matches later.
