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) and that you know your way around the blueprint canvas (see Blueprint 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:
{
"created": 1752768000,
"owner_guid": "a23575f49d0af385314c1f02280163374297e018a692e5e4ab85eb307ebf6ebc",
"owner_name": "joe doe",
"status": "open",
"title": "write the release notes"
}Two 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_nameso the list page can show it without a second lookup. We keepowner_guidas the reference for when we do need the full user document.
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.
2. Create a Task
Create a new blueprint of type Build a UI Page — this seeds the canvas with a state_page block. Build a small form with a state_form containing a state_input_dynamic (field title) and a state_button. When the button is clicked, the create flow runs:
- Get the submitted values with
state_form_get, which outputs the form data as an object. - Add a
storageblock and select yourtasksstorage. - Add a
mutations_set_bp_field_multipleblock and select the fields to set:titlefrom the form,status=open,created= now, and the owner fields from the current user. - Add a
function_create_documentblock. 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
},
"created": 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:
| Block | Purpose | Connects To |
|---|---|---|
storage (tasks) |
Selects the storage to search in | function_search (storage pin) |
query_bool_term_fields (status = open) |
Match only open tasks | query_bool_filter |
query_bool_filter |
Wrap the term query in an efficient filter context | query_and |
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: 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 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 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 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 with the task's guid and a 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 for the full list.
For deletion you have two options:
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 withfunction_restore_document.function_delete_document— permanent delete. The document is gone for good (unless revisions are enabled on the storage).
remove (soft delete) in most cases. It allows recovery when a user deletes a task by accident, and you can permanently purge removed documents later with a repeating event after a retention period. 6. Add Search
Finally, let users find tasks by title. Add a 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 (field: title) |
Full-text match of the user's input on title |
query_and |
query_bool_filter (status = open) |
Keep the open-tasks filter from step 3 | query_and |
query_sort_field (_meta.created, desc) |
Keep the sorting | query_and |
query_and |
Combine text query, filter, and sort | function_search (query pin) |
Report will not match report. 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.
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:
Storage Go deeper on query types, denormalization strategies, document lifecycle, and caching. Storage Events React to creates, updates, and removals — for example to sync denormalized owner names. Cluster APIs Expose your tasks to external services through 40+ REST APIs.





