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:

Task Document
{
  "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_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.

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:

  1. Get the submitted values with state_form_get, which outputs the form data as an object.
  2. Add a storage block and select your tasks storage.
  3. Add a mutations_set_bp_field_multiple block and select the fields to set: title from the form, status = open, created = now, and the owner fields from the current user.
  4. Add a function_create_document block. Connect the storage and mutations to its in-pins, and connect the flow pin from the button's click event.
Create flow: form get, mutations, and create document blocks connected to the tasks storage

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:

Created Task Output
{
  "_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)
Search query blocks connected to function_search on the tasks storage

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.

The rendered tasks list page with the advanced datatable showing open tasks

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.

Update flow: mutations setting status to done, connected to function_update_document with the task guid

For deletion you have two options:

  • function_remove_documentsoft 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.
  • function_delete_documentpermanent delete. The document is gone for good (unless revisions are enabled on the storage).
Best Practice Use 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)
Search input on the list page wired to a simple query string block and function_search
Case-Sensitive Searches Storage searches are case-sensitive. Since we stored titles in lowercase, convert the user's input to lowercase before connecting it to the query block, or 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.