Storage Events

When dealing with storage operations, it's important to note that document changes can impact the entire application. That's why we provide the ability to manage storage events, enabling you to trigger events when documents are created, updated, or removed within your storage system.

Supported Event types

At present, we offer support for the following storage events:

Event Description
on_created This event is triggered when a new document is created within the specified storage.
on_saved This event is triggered when a new document is created or an existing document is updated within the specified storage.
on_updated This event is triggered when an existing document is updated within the specified storage.
on_removed This event is triggered when an existing document is marked as removed within the specified storage.

Event Execution

Storage Events operate on a First In, First Out (FIFO) basis, although simultaneous execution by multiple nodes is possible. For instance, while Node A is processing Event 61, Node B might already be executing Event 62, due to Node A concurrently processing Event 60, which Node B is not. Given the variability in processing speeds across different nodes, the FIFO execution logic for these events cannot be guaranteed.

Within the blueprint function, you can manage the execution order using lock & wait blocks. However, this approach may decelerate other events that are executing in the same thread and block. To ensure a 100% guarantee of execution order, it is advisable to offload the intensive tasks of Storage Events and transfer them to a queue.

storage events execution
Execution Order
Execution Order

Event Payload

When a document is created or updated, we will furnish you with the most recent document data, along with a list of fields that have undergone modifications. The document you receive represents the current state of the document at the moment of the event.

Given the extremely rapid and high-performance nature of these events, they may occasionally be combined. To provide you with a clearer understanding of what might occur in such rare scenarios, we've included a JSON example below illustrating the functioning of Storage Events.

Document Created Flow

Document Created
{
  "username": "Joe",
  "_meta": {
    "guid": "a23575f49d0af385314c1f02280163374297e018a692e5e4ab85eb307ebf6ebc",
    "expiry": -1,
    "entity": 1,
    "removed": 0,
    "created": 1706396400,
    "updated": 1706396400,
    "ums": 1706396400803,
    "cms": 1706396400362,
    "update_hash": "60e27209fa93063b5605e41605c2722ed428cae0"
  }
}
Storage Created Event
{
  "object": {
    "username": "Joe",
    "_meta": {
      "guid": "a23575f49d0af385314c1f02280163374297e018a692e5e4ab85eb307ebf6ebc",
      "expiry": -1,
      "entity": 1,
      "removed": 0,
      "created": 1706396400,
      "updated": 1706396400,
      "ums": 1706396400803,
      "cms": 1706396400362,
      "update_hash": "60e27209fa93063b5605e41605c2722ed428cae0"
    }
  },
  "revisions": [
    {
      "key": "username",
      "value": "Joe",
      "previous_value": null
    }
  ],
  "fields_modified": [
    "username"
  ]
}

Document Updated Flow

Document Updated
{
  "username": "Joe Do",
  "firstname": "Joe",
  "lastname": "Do",
  "_meta": {
    "guid": "a23575f49d0af385314c1f02280163374297e018a692e5e4ab85eb307ebf6ebc",
    "expiry": -1,
    "entity": 1,
    "removed": 0,
    "created": 1706396400,
    "updated": 1706482800,
    "ums": 1706482800417,
    "cms": 1706396400362,
    "update_hash": "8fc1d0b6a4e37225b1904c7de0f5a8c6431bb27e"
  }
}
Storage Updated or Saved Event
{
  "object": {
    "username": "Joe Do",
    "firstname": "Joe",
    "lastname": "Do",
    "_meta": {
      "guid": "a23575f49d0af385314c1f02280163374297e018a692e5e4ab85eb307ebf6ebc",
      "expiry": -1,
      "entity": 1,
      "removed": 0,
      "created": 1706396400,
      "updated": 1706482800,
      "ums": 1706482800417,
      "cms": 1706396400362,
      "update_hash": "8fc1d0b6a4e37225b1904c7de0f5a8c6431bb27e"
    }
  },
  "revisions": [
    {
      "key": "username",
      "value": "Joe Do",
      "previous_value": "Joe"
    },
    {
      "key": "firstname",
      "value": "Joe",
      "previous_value": null
    },
    {
      "key": "lastname",
      "value": "Do",
      "previous_value": null
    }
  ],
  "fields_modified": [
    "username",
    "firstname",
    "lastname"
  ]
}

Document Fast Updated Flow

It's worth noting that when updates occur at high speed, the triggered events may be consolidated. Below is an example of such a combined event. If a key has been modified multiple times, it will appear multiple times in the revisions array.

Document Updated 1
{
  "username": "Joe",
  "firstname": "Joe",
  "_meta": {
    "guid": "a23575f49d0af385314c1f02280163374297e018a692e5e4ab85eb307ebf6ebc",
    "expiry": -1,
    "entity": 1,
    "removed": 0,
    "created": 1706396400,
    "updated": 1706482800,
    "ums": 1706482800417,
    "cms": 1706396400362,
    "update_hash": "8fc1d0b6a4e37225b1904c7de0f5a8c6431bb27e"
  }
}
Document Updated 2
{
  "username": "Joe",
  "lastname": "Do",
  "_meta": {
    "guid": "a23575f49d0af385314c1f02280163374297e018a692e5e4ab85eb307ebf6ebc",
    "expiry": -1,
    "entity": 1,
    "removed": 0,
    "created": 1706396400,
    "updated": 1706482800,
    "ums": 1706482800419,
    "cms": 1706396400362,
    "update_hash": "b7440ac2915de6f038c1a55eb9376d024f8ce1a3"
  }
}
Storage Updated or Saved Event
{
  "object": {
    "username": "Joe",
    "firstname": "Joe",
    "lastname": "Do",
    "_meta": {
      "guid": "a23575f49d0af385314c1f02280163374297e018a692e5e4ab85eb307ebf6ebc",
      "expiry": -1,
      "entity": 1,
      "removed": 0,
      "created": 1706396400,
      "updated": 1706482800,
      "ums": 1706482800419,
      "cms": 1706396400362,
      "update_hash": "b7440ac2915de6f038c1a55eb9376d024f8ce1a3"
    }
  },
  "revisions": [
    {
      "key": "firstname",
      "value": "Joe",
      "previous_value": null
    },
    {
      "key": "lastname",
      "value": "Do",
      "previous_value": null
    }
  ],
  "fields_modified": [
    "firstname",
    "lastname"
  ]
}

Write Loops

A flow triggered by a storage event can itself write documents, and those writes raise storage events of their own. This composes across blueprints: if the on_saved flow for storage A updates a document in storage B, and the on_saved flow for storage B updates a document in storage A, the two flows feed each other forever. Each blueprint is locally correct, and because the studio shows one blueprint at a time, the cycle never appears on any canvas: it only exists in the composition. The same applies to a single flow that re-saves a document in its own storage.

Guard your writes

A store-write block inside a storage event flow should do one of two things:

  • Skip the events it would raise. Pass skip_on_saved in the options of function_update_document or function_create_document. The write happens, no storage events fire for it, and the chain ends there. This is the right default for derived or mirrored data.
  • Cascade on purpose, with a termination condition. If downstream flows must react to the write, add a data condition that makes the second pass a no-op. For example, compare the value before writing and only write when it actually changes. Without such a condition, the cascade is an infinite loop by construction.

Loop cancellation

The platform cancels runaway chains, so a loop starves within one generation instead of running forever. Two independent checks apply:

  • By identity. Every storage event carries its chain's lineage: which storage and document raised each earlier hop. A flow may re-enter its own document once; that is what the termination-condition pattern relies on. The second time the same document on the same storage re-enters its own chain, the event is cancelled with BLUEPRINT_LOOP. A two-storage cycle dies after about 5 hops, a self-loop after about 3.
  • By depth. Independently of identity, any chain deeper than 10 storage-event hops is cancelled the same way. This backstop covers loops the identity check cannot see, such as flows that create a new document each generation.

A cancelled event runs no flows, so it raises no further writes and the chain produces no next generation.

Reading the log

Log line Meaning
Informational line naming a suspected cycle Your flow re-entered a document it already wrote in this chain. It still ran, because one revisit is allowed, but a second re-entry will be cancelled. Treat it as an early warning: check whether the cascade is intentional and has a termination condition.
BLUEPRINT_LOOP warning The platform cancelled a runaway chain. The line names the storages and documents in the cycle; that is where to find the blueprints to fix.

The cancellation is a safety net, not a design tool

A loop that gets cancelled has still run several generations of unnecessary writes before it dies. Design storage event flows so the chain ends on its own: skip the events on derived writes, and give every deliberate cascade a termination condition. See also Common Pitfalls.

Frequently asked

Which storage events does RUAL support?

Four: on_created when a new document is created, on_updated when an existing document is updated, on_saved for either, and on_removed when a document is marked as removed. Each fires a flow through the storage_event block.

Do RUAL storage events run in order?

Not guaranteed. Events are processed first in, first out, but multiple nodes execute them simultaneously at different speeds, so strict FIFO order cannot be guaranteed. For a hard execution order, offload the intensive work to the queue.

What data does a RUAL storage event contain?

The most recent document data, a revisions array with the key, value and previous_value of each modified field, and a fields_modified list. When updates happen at high speed, events may be consolidated into one.

What does BLUEPRINT_LOOP mean in RUAL?

The platform cancelled a runaway storage-event chain: two storage event flows were writing to each other's storage, or a chain grew deeper than 10 storage-event hops. The log line names the storages and documents in the cycle. Fix the flows by passing skip_on_saved on the store write, or give a deliberate cascade a termination condition.