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.
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
| Attribute | Meaning | Exam-relevant behavior |
|---|---|---|
persistent | true survives a runtime restart; false keeps entries in memory only | The implicit default store is persistent; an in-memory store is emptied by every restart or redeploy |
maxEntries | Maximum number of keys retained | When the ceiling is reached the store evicts the oldest entries |
entryTtl / entryTtlUnit | How long an entry stays valid | An entry past its TTL is eligible for removal, not instantly removed |
expirationInterval / expirationIntervalUnit | How often the expiration sweeper runs | An 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, andexpirationIntervalare mutually dependent: configure all three or none of them. Declaring onlyentryTtlis a configuration error, and it is a favourite distractor in "which XML is valid?" questions.
4. The Operation Set
| Operation | XML element | Payload / effect | Error when it goes wrong |
|---|---|---|---|
| Store | <os:store key="…"> | Writes a value; overwrites by default | OS:KEY_ALREADY_EXISTS when failIfPresent="true"; OS:NULL_VALUE on a null value |
| Retrieve | <os:retrieve key="…"> | Returns the stored value as the new payload | OS:KEY_NOT_FOUND if the key is absent and no <os:default-value> is set |
| Contains | <os:contains key="…"> | Returns a Boolean | OS:INVALID_KEY on a null/blank key |
| Remove | <os:remove key="…"> | Deletes one entry | OS:KEY_NOT_FOUND if the key is absent |
| Retrieve All Keys | <os:retrieve-all-keys/> | Returns an Array of key strings | OS:STORE_NOT_AVAILABLE |
| Retrieve All | <os:retrieve-all/> | Returns the whole store as an Object | OS:STORE_NOT_AVAILABLE |
| Clear | <os:clear/> | Empties the store | OS: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 constraint | Published limit |
|---|---|
| Maximum key length | 1024 bytes (UTF-8) |
| Maximum value size | 10 MB (Base64-encoded) |
| Maximum TTL | 2,592,000 seconds (30 days) |
| Number of keys per application | No 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.
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 developer declares <os:object-store name="rateCache" entryTtl="15" entryTtlUnit="MINUTES"/> and the application fails to deploy. What is wrong with this configuration?
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 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?