Developers tend to dismiss low-code tools, and I understand why: what you see first is boxes connected by arrows, which looks like flowchart software rather than programming. But that visible layer is only half the platform. Underneath every one of those boxes is an expression language, and that is where the actual logic lives.

The distinction that made this click for me is the old one between statements and expressions. Statements do things. Expressions evaluate to values. In Power Automate the actions are statements, and their fields accept expressions.

Once you notice that, a whole category of clumsy flows becomes avoidable. This:

Initialize variable  ->  Condition  ->  Set variable (true branch)
                                    ->  Set variable (false branch)

is four actions to express what one expression says:

if(equals(triggerBody()?['status'], 'approved'), 0.15, 0.05)

Same logic, dropped straight into whichever field needs the number.

What it buys you

Fewer moving parts. Four actions become one. Fewer things to name, reorder, or accidentally leave dangling when the requirement changes.

Less run history. Every action records its inputs and outputs on every run. Collapsing four actions into one expression removes three sets of that bookkeeping from every execution, which is where the speed difference comes from. On a flow that runs constantly, this is not a micro-optimisation.

Logic in one place. When the discount rule changes, you edit one expression rather than restructuring a branch, and the diff is one line.

Here is a nested one that does real work, calculating a due date from a priority in a single value:

formatDateTime(
  addDays(utcNow(), if(equals(triggerBody()?['priority'], 'high'), 1, 3)),
  'yyyy-MM-dd'
)

Three functions composed into one value: a conditional feeding an arithmetic function feeding a formatter. That is functional composition, in a tool people describe as not programming.

The honest downside, and the technique that fixes it

Expressions get hard to debug. A deeply nested one that returns the wrong answer gives you no stack trace and no intermediate values, just a wrong result and a long line of parentheses.

What works is borrowed straight from a REPL: build it in pieces, in separate Compose actions. Compose does nothing except evaluate an expression and show you the result. Put the inner call in its own Compose, run the flow, look at the output. Then the next layer out. When each piece is confirmed, collapse them into the single expression, or leave them separate if the clarity is worth more than the actions.

That gives you the thing the expression editor otherwise lacks: a way to inspect intermediate state. It is the same instinct as printing a variable mid-function, and it turns expression debugging from guesswork into stepping.

The point

The mistake is judging a platform by its most visible layer. Yes, the drag-and-drop surface is limited; it is meant to be, because it exists so that non-programmers can assemble something that works. But the expression language underneath is a real, composable, functional language, and knowing it is the difference between a flow that is a wall of forty boxes and one that is eight boxes doing the same job.

If you already write code, you are unusually well placed to use these tools well, because expression thinking is a habit you already have. You just have to look one level below the arrows to find where it applies.