What happens to your C# when the runtime changes
In O11 an extension runs inside the platform. In ODC custom code runs beside it, over the network, with no state and no database. That difference is what turns an extension list into a timeline, and no compatibility score can read inside the assembly.
On this pageWhat an extension is in O11
Every mature O11 estate has an extension list. Usually it is short, twenty or thirty items, and usually nobody has read it in years, because extensions are the part of the platform that works quietly right up until the day it does not.
When the estate is assessed for ODC, that list gets a line in the summary. Extensions: 27. Effort: to be confirmed. Then the rest of the document is about applications, because applications are where the tooling has something to say.
We think the extension list is the timeline. This is why.
What an extension is in O11
In O11 an extension is a piece of .NET that runs inside the platform. You write it in Integration Studio, it compiles to an assembly, and that assembly is deployed to the front-end server alongside the modules that consume it. When a server action calls an extension action, that is an in-process call. Same machine, same request, same transaction.
That gives extension code a set of privileges nobody thinks of as privileges, because they were never labelled as such.
It can talk to the database directly, on the platform’s own connection, inside the request that called it. It can keep state between calls, because the class lives in the server process rather than being created fresh each time. It can read and write the server’s file system. It can hold a connection open to something on the network and reuse it. It can run for as long as the request allows. And through Integration Studio it can map tables in an external database into entities that the rest of the estate consumes as if they were its own.
Most extensions use none of that. Some use all of it. From the outside they look identical: a name, a version, a list of actions.
What custom code is in ODC
In ODC, custom code does not run inside the platform. It runs beside it.
An external logic library is C# on .NET 8, built in your own IDE against the OutSystems SDK, compiled, zipped and uploaded to the ODC Portal. Only the compiled output goes up. The source stays with you. Each public method becomes a serverless function, and when an ODC server action calls it, the call goes over the network to that function and back.
That is a different thing, and the differences are the conversion.
There is no database inside the function. External logic cannot read or write ODC entities. Whatever your extension was doing against the database has to come back into the visual layer or go somewhere else entirely.
There is no state. Each invocation stands on its own. Anything cached in a static field, any pooled connection, any counter, any load-it-once-on-first-call pattern, is gone.
There is no local disk that means anything, no front-end server, and no shared transaction with the caller.
And there are limits with numbers on them. Request and response payloads are capped at a little over five megabytes, because that is what the underlying serverless platform allows. A function that runs long enough hits a hard ceiling, documented at ninety-five seconds, and the server request that called it has a much shorter budget by default. The first call after a quiet period is slower than the rest, because the function is being warmed.
None of this is a complaint about ODC. Running custom code out of process is the right design for a runtime where apps are independently deployed containers and one bad extension should not be able to take a front-end server down with it. Version pinning, isolation, scaling that does not eat the app’s own capacity, those are real gains, and any estate that has lost an afternoon to a misbehaving extension will feel them.
It is just not what your extensions assume.
Where the extension list becomes a timeline
Put the two pictures next to each other and every extension in the estate falls into one of a small number of buckets. The buckets, not the count, are what set the timeline.
Wrappers. The extension exists to call a .NET library the platform does not expose: a PDF generator, a spreadsheet writer, a cryptography routine, a cloud provider’s SDK. It takes inputs, does a thing, returns outputs, and touches nothing else. These port. The code changes are mostly mechanical, the SDK attributes replace the Integration Studio metadata, and the questions left are about payload size and cold start. This is the majority of most extension lists, and it is the part the effort estimate was quietly assuming was all of it.
Platform glue. The extension reaches into things only an in-process extension can reach. It queries the database directly. It runs a bulk insert the visual layer was too slow for. It reads session, or files on the server, or holds a connection open across calls. None of that has an equivalent in an external logic function. The logic does not port. It gets redesigned, and usually the redesign is that the work moves back into visual logic or into a different mechanism altogether.
External entities. Not C# at all, but they live in Integration Studio, so they sit on the same list. Tables in another database, mapped in as entities and consumed across the estate like any other. In ODC that becomes a Data Fabric connection, configured in the portal, with its own rules about what is exposed and how. Every module that references one of those entities is now a consumer of an external connection, and that is a different dependency from the one it had before.
Long running and large. Anything that processes a file over five megabytes, or runs for more than a minute and a half, or leans on the caller’s request staying open. These do not fail on paper. They fail in the third week of testing, on the production-sized file, and the fix is chunking, a store-and-reference pattern, or moving the work onto an asynchronous path. All of which is design.
Somebody else’s. Forge components with an extension inside them. The question is whether an ODC version exists and whether you can adopt it, and that depends on a second question the estate has to answer honestly: was the O11 component cloned and changed before anyone made a note of it. If it was, it is yours now, and it goes into whichever bucket above it belongs in.
No source. The extension is a compiled assembly and the .NET project that produced it left with the person who wrote it. This bucket is small and it is the most expensive one on the list, because in ODC only compiled output is uploaded and nothing on the platform will decompile a DLL for you. Whatever it does has to be rediscovered from the outside and written again.
Why the tooling cannot see this
A compatibility score reads the metamodel. It can see that an application references an extension, how many actions it calls, and roughly how often. It cannot see inside the assembly, and inside the assembly is where the bucket is decided. A wrapper and a piece of platform glue look identical from the visual layer: same shape, same inputs, same outputs, same number of call sites.
So the list in the summary is a list. It is not an estimate and it is not a plan, and the two things that turn it into either are reading the code and asking the team what each extension was for. Both of those are people, and the second one is people who may already have left.
What we count
Before anyone puts a number on the conversion, we go through every extension and write down four things.
Which bucket. That one decision moves the timeline more than anything else on the page.
Whether the source exists and builds. Not whether it is in the package. Whether someone can open it today and get an assembly out.
What it touches. Database, file system, session, network, state. Anything on that list and it is not a wrapper, whatever it looks like.
Who consumes it. Module count and call site count, separately, because a wrapper used from one place and a wrapper used from forty are the same port and very different regression tests.
Four answers per extension, and the number of items sitting in the glue, long-running and no-source buckets is what we would set the timeline from. Not the total. It is the same exercise as the producer module inventory, applied to the code that inventory cannot see.
The application list tells you how much there is to convert. The extension list tells you how long it will take. They are different documents, and only one of them tends to get read.