7.4 Object Store: Persisting Data Between Flow Executions

Key Takeaways

  • Every Mule 4 application gets an implicit default object store that is persistent and requires no configuration; you only declare <os:object-store> when you need custom TTL, capacity, or in-memory behavior.
  • The core operations are <os:store>, <os:retrieve>, <os:contains>, <os:remove>, <os:retrieve-all-keys>, <os:retrieve-all>, and <os:clear>, all in the os: namespace.
  • Retrieving a missing key raises OS:KEY_NOT_FOUND unless you supply a <os:default-value> child element; storing over an existing key with failIfPresent="true" raises OS:KEY_ALREADY_EXISTS.
  • entryTtl, maxEntries, and expirationInterval must be configured together — supplying only one is a configuration error, and expiration only happens when the sweeper runs.
  • On CloudHub, worker disks are ephemeral, so a persistent object store is backed by Object Store v2: values up to 10 MB, keys up to 1024 bytes, and a maximum TTL of 30 days.
Last updated: August 2026

Object Store: Persisting Data Between Flow Executions

A Mule event lives and dies inside a single flow execution. Its payload, attributes, and variables are discarded the moment the flow completes, so nothing a flow computes is visible to the next execution of that same flow. Yet many integration patterns depend on exactly that kind of memory: remembering the last synchronized timestamp, caching an OAuth token until it expires, deduplicating a message ID that a broker redelivered, or counting attempts before giving up. The Salesforce Certified MuleSoft Developer I blueprint calls this out as its own objective — apply correct processors/syntax to persist data between flow executions — and the component that satisfies it is the Object Store.


1. Why Variables Are Not Enough

+-----------------------------------------------------------------------------------------+
|                       VARIABLE LIFETIME vs. OBJECT STORE LIFETIME                        |
|                                                                                         |
|   EXECUTION 1 (10:00:00)            EXECUTION 2 (10:05:00)                               |
|   +--------------------------+      +--------------------------+                        |
|   | vars.lastId = 4021       |      | vars.lastId = (undefined)|  <-- vars did NOT carry |
|   | os:store 'lastId' = 4021 |      | os:retrieve 'lastId'     |      over               |
|   +--------------------------+      |   --> 4021               |  <-- object store DID   |
|              |                      +--------------------------+                        |
|              v                                   ^                                      |
|      [ OBJECT STORE: { "lastId": 4021 } ] -------+                                       |
|      (survives flow end; survives restart when persistent="true")                        |
+-----------------------------------------------------------------------------------------+

An exam question that describes a scheduled flow that must not reprocess records it already handled, or an HTTP token that should be reused until it expires, is asking for an Object Store. Distractors typically offer a flow variable (destroyed at flow end), a Set Payload (destroyed just as fast), or writing to the worker's local disk (destroyed when a CloudHub worker restarts).


2. The Implicit Default Store

Every Mule application ships with a default object store that is persistent and available without any configuration. If you drop an <os:store> operation into a flow and never declare a store, that default store is what you get.

<flow name="record-last-sync-flow">
    <scheduler doc:name="Every 5 Minutes">
        <scheduling-strategy>
            <fixed-frequency frequency="5" timeUnit="MINUTES"/>
        </scheduling-strategy>
    </scheduler>

    <!-- Read the watermark saved by the PREVIOUS execution -->
    <os:retrieve key="lastSyncTimestamp" target="lastSync" doc:name="Retrieve Watermark">
        <os:default-value><![CDATA[#[|1970-01-01T00:00:00Z|]]]></os:default-value>
    </os:retrieve>

    <db:select config-ref="Database_Config" doc:name="Select Changed Rows">
        <db:sql>SELECT * FROM orders WHERE updated_at > :since</db:sql>
        <db:input-parameters><![CDATA[#[{'since': vars.lastSync}]]]></db:input-parameters>
    </db:select>

    <!-- Write the new watermark for the NEXT execution -->
    <os:store key="lastSyncTimestamp" doc:name="Store Watermark">
        <os:value><![CDATA[#[now()]]]></os:value>
    </os:store>
</flow>

Two details in that snippet are heavily tested. First, <os:retrieve> writes the retrieved value into the payload by default; the target attribute is what redirects it into vars.lastSync and leaves the payload untouched. Second, without the <os:default-value> child, the very first execution — when the key does not exist yet — fails with OS:KEY_NOT_FOUND.


3. Declaring a Custom Object Store

When you need a bounded cache, an expiry policy, or an explicitly in-memory store, declare one globally with <os:object-store> and point operations at it with the objectStore attribute.

<os:object-store name="tokenCache"
                 persistent="false"
                 maxEntries="500"
                 entryTtl="30"
                 entryTtlUnit="MINUTES"
                 expirationInterval="1"
                 expirationIntervalUnit="MINUTES"
                 doc:name="Token Cache"/>

<flow name="get-oauth-token-flow">
    <os:contains key="crmAccessToken" objectStore="tokenCache" target="hasToken" doc:name="Token Cached?"/>

    <choice doc:name="Cached or Fetch">
        <when expression="#[vars.hasToken]">
            <os:retrieve key="crmAccessToken" objectStore="tokenCache" doc:name="Reuse Token"/>
        </when>
        <otherwise>
            <http:request method="POST" config-ref="Auth_Config" path="/oauth/token" doc:name="Fetch New Token"/>
            <os:store key="crmAccessToken" objectStore="tokenCache" doc:name="Cache Token">
                <os:value><![CDATA[#[payload.access_token]]]></os:value>
            </os:store>
            <set-payload value="#[payload.access_token]" doc:name="Token Only"/>
        </otherwise>
    </choice>
</flow>

Configuration Attributes

AttributeMeaningExam-relevant behavior
persistenttrue survives a runtime restart; false keeps entries in memory onlyThe implicit default store is persistent; an in-memory store is emptied by every restart or redeploy
maxEntriesMaximum number of keys retainedWhen the ceiling is reached the store evicts the oldest entries
entryTtl / entryTtlUnitHow long an entry stays validAn entry past its TTL is eligible for removal, not instantly removed
expirationInterval / expirationIntervalUnitHow often the expiration sweeper runsAn entry with a 30-minute TTL and a 1-hour sweep can survive up to ~90 minutes

[!WARNING] The Three Attributes Travel Together entryTtl, maxEntries, and expirationInterval are mutually dependent: configure all three or none of them. Declaring only entryTtl is a configuration error, and it is a favourite distractor in "which XML is valid?" questions.


4. The Operation Set

OperationXML elementPayload / effectError when it goes wrong
Store<os:store key="…">Writes a value; overwrites by defaultOS:KEY_ALREADY_EXISTS when failIfPresent="true"; OS:NULL_VALUE on a null value
Retrieve<os:retrieve key="…">Returns the stored value as the new payloadOS:KEY_NOT_FOUND if the key is absent and no <os:default-value> is set
Contains<os:contains key="…">Returns a BooleanOS:INVALID_KEY on a null/blank key
Remove<os:remove key="…">Deletes one entryOS:KEY_NOT_FOUND if the key is absent
Retrieve All Keys<os:retrieve-all-keys/>Returns an Array of key stringsOS:STORE_NOT_AVAILABLE
Retrieve All<os:retrieve-all/>Returns the whole store as an ObjectOS:STORE_NOT_AVAILABLE
Clear<os:clear/>Empties the storeOS:STORE_NOT_AVAILABLE

Idempotent Consumption Pattern

The classic combination — used to discard a JMS or HTTP message the application has already seen — pairs contains with store:

<flow name="idempotent-order-intake">
    <jms:listener config-ref="JMS_Config" destination="orders" doc:name="On Order"/>

    <os:contains key="#[payload.orderId]" target="alreadySeen" doc:name="Seen Before?"/>

    <choice doc:name="Duplicate Check">
        <when expression="#[vars.alreadySeen]">
            <logger level="WARN" message="#['Discarding duplicate order ' ++ payload.orderId]" doc:name="Log Duplicate"/>
        </when>
        <otherwise>
            <os:store key="#[payload.orderId]" doc:name="Mark Processed">
                <os:value><![CDATA[#[now()]]]></os:value>
            </os:store>
            <flow-ref name="processOrderFlow" doc:name="Process Order"/>
        </otherwise>
    </choice>
</flow>

Note where the key comes from: a DataWeave expression on the message, not a literal. Keys are strings, and expressions that resolve to a number must be coerced (#[payload.orderId as String]) when the source system supplies numeric IDs.


5. Object Store v2 on CloudHub

CloudHub worker disks are ephemeral, so "persistent" on CloudHub does not mean "written to the worker's file system". A persistent store on CloudHub is backed by Object Store v2 (OSv2), a shared cloud service that is also what lets multiple workers of the same application see the same keys.

OSv2 constraintPublished limit
Maximum key length1024 bytes (UTF-8)
Maximum value size10 MB (Base64-encoded)
Maximum TTL2,592,000 seconds (30 days)
Number of keys per applicationNo documented limit

[!IMPORTANT] Multi-Worker State Lives in OSv2, Not in Memory If an exam scenario deploys an application across two or more CloudHub workers and asks how a counter, token, or watermark stays consistent, the answer is a persistent object store backed by Object Store v2. An in-memory store (persistent="false") gives each worker its own private copy, so the two workers disagree — a classic wrong-answer trap dressed up as "faster".

[!TIP] Object Store vs. Batch vs. Cache Scope The Cache scope also uses an object store underneath, but it caches a computed result keyed by an expression. When the question describes remembering a value across separate flow executions, choose the Object Store connector. When it describes avoiding recomputation of an expensive downstream call within the same request pattern, the Cache scope is the better fit.

Test Your Knowledge

A scheduled Mule flow polls an API every 10 minutes and must not reprocess records it already retrieved. On the very first execution the flow calls <os:retrieve key="lastRunTimestamp"/> and the application fails immediately. What is the error, and what is the minimal fix?

A
B
C
D
Test Your Knowledge

A developer declares <os:object-store name="rateCache" entryTtl="15" entryTtlUnit="MINUTES"/> and the application fails to deploy. What is wrong with this configuration?

A
B
C
D
Test Your Knowledge

An application is deployed to CloudHub 1.0 with three workers behind the shared load balancer. It stores a per-customer request counter in an object store declared with persistent="false". Support reports that a customer who has clearly exceeded the limit is still being served. What explains this behavior?

A
B
C
D
Test Your Knowledge

A Mule flow consumes messages from a JMS queue whose broker occasionally redelivers a message. The developer must discard any message whose orderId has already been processed, while still processing genuinely new orders. Which combination of Object Store operations implements this?

A
B
C
D