The OutSystems Inconlistency: When Your Lists Don't Know the Rules
- Greg Whitten

- 1 day ago
- 5 min read

A tale of Advent of Code, a shortest-path algorithm gone rogue, and a platform behaviour so silent you'd never know it was there... until it wasn't.
It started with Advent of Code
Every December, developers worldwide willingly sacrifice their sleep to Advent of Code - a series of daily programming puzzles that runs from December 1st through Christmas Day. It's the season of giving, and apparently we give our evenings to debugging.
I decided to tackle the challenge in OutSystems one year. Partly as a learning exercise, partly as a way to push the platform in directions it wasn't necessarily designed for, and partly because apparently I enjoy suffering. One of the puzzles called for a shortest-path algorithm - the kind of thing that leans heavily on lists of nodes, visited states, and careful mutation management.
It was producing completely wild results. Paths that didn't exist. Nodes visited twice. Lists that seemed to change between the moment I passed them and the moment I read them back. The algorithm wasn't broken. My logic wasn't broken. Something else was going on.
After enough digging, I found it: my lists were being mutated in ways I hadn't asked for, by behaviour I didn't know existed, that isn't documented anywhere obvious.
OutSystems was silently passing my List parameters By Reference - and I had absolutely no idea.
I've taken to calling what I discovered the "Inconlistency". You'll see why.
A quick primer: By Copy vs. By Reference
A quick refresher for anyone who hasn't had to think about this since their computer science days. When you pass a variable to a function, there are two broad approaches:
By Copy means the function receives its own independent copy of the data. If the function modifies it, the original is untouched. What happens in the function, stays in the function.
By Reference means the function receives a pointer to the original data. Modifications inside the function affect the original directly. It's like handing someone your house keys - if they rearrange the furniture, you'll know about it when you get home.
In OutSystems, parameters are passed By Copy. That's the rule, and as rules go, it's a good one. Predictable, safe, no surprises. Except there is one place where OutSystems quietly breaks it, with no setting to configure, no indicator in the UI, and no mention in the docs.
The hidden exception... and the Inconlistency
Here's the complete picture of how OutSystems actually handles parameter passing across action types:
Action type | Parameter type | What actually happens |
Client action | Any, including List | By Copy — always, no exceptions |
Server action | Non-List types | By Copy — always, no exceptions |
Server action | List — called from same module | By Reference — silently, invisibly |
Server action | List — called from external module | By Copy — back to the platform norm |
Row 3 is the Inconlistency. Everything else in the platform behaves exactly as you'd expect - By Copy, consistent, predictable. But List parameters passed to a server action from within the same module? Silently By Reference. No property to set. No indicator in Service Studio. It just happens, and you are expected to either already know about it or discover it the hard way.
(Hi. I discovered it the hard way. That's what this post is.)
And then it gets worse
Row 4 is where the Inconlistency reveals its full consequences.
OutSystems lets you make server actions public and expose them to other modules - a common, encouraged, entirely normal pattern for building modular applications. The moment a different module calls your public server action, the List parameter silently reverts to By Copy. The platform norm reasserts itself. Which sounds fine - except now the exact same action behaves differently depending on where the call originates.
Called from the defining module → List is By Reference. Mutations inside the action affect the caller's original list.
Called from an external module → List is By Copy. Mutations inside the action are isolated. The caller's list is untouched.
The action has no way to know which world it's living in at runtime. It cannot inspect its calling context. It cannot adapt. It just behaves differently depending on an invisible external factor - which is roughly the definition of a bad time.
Why does this actually matter?
The intended benefit of By Reference passing is that an action can modify a list in place and have those changes reflected in the caller, without needing to explicitly return anything. It's a performance convenience. But that only works if the behaviour is consistent and knowable - and since it isn't, you can't safely use it as a feature.
If you write your action relying on By Reference semantics - using in-place list mutations to communicate results back to the caller - it will work perfectly from within the same module, and silently fail when called externally. No error. No warning. Just a list that didn't update, and a debugging session you didn't budget for.
Conversely, if you write defensive code to avoid mutating the caller's list - treating it as read-only out of caution - you're doing extra work that the platform happily discards for you when called from another module anyway.
You lose the upside of By Reference. You still have to defend against its downsides.
The worst of both worlds, wrapped in a festive bow. 🎄
What should you do about it?
Until this behaviour is made consistent, the safest approach is to always treat List parameters in server actions as if they are passed By Copy - regardless of where you're calling from.
Practically, that means:
Never rely on in-place list mutations to communicate results back to the caller. Use output parameters or return values instead - explicit, unambiguous, and consistent regardless of module context.
If your action modifies a list internally and you don't want to affect the caller's data, make a local copy at the start of the action and work on that. Treat the input as read-only.
Tell your team. This is exactly the kind of thing that costs hours when someone encounters it cold. A five-minute conversation now saves a very confused afternoon later.
These are good defensive programming habits in any context - but here they're load-bearing. The platform won't warn you when you're in the danger zone, so you have to build the guardrails yourself.
A note on OutSystems
Every mature platform has its quirks, and this is one of OutSystems'. The By Reference behaviour for Lists almost certainly exists for a legitimate performance reason, and in the vast majority of day-to-day development you'll never encounter it. It only bites you at a very specific intersection: public server actions, List parameters, cross-module consumption - and even then, only if you're leaning on mutation-based patterns.
But when it does bite, it bites hard. So now you know.
Saving you from this particular debugging nightmare in the middle of summer feels like the least festive Christmas gift imaginable... but here we are.
You're welcome. 🎄
Have you run into this behaviour yourself? Or spotted another OutSystems quirk worth writing about?
Drop a comment or reach out - we'd love to hear about it.
end of article.




Comments