By only exporting simple functions (which only take DSL commands and return DSL commands) out of the modules - it works well with Racket and some other Scheme implementations, for example. In Clojure you can simulate this with namespaces and an explicit "(require ...)" which can be easily followed by the static tools.
This way every name is either locally defined or coming from one of the modules you explicitly required, and with no higher order functions allowed in between modules you can track everything to its source.
> let's say for a UI form case?
That's the most classic example of this approach, used more widely than any other message DSL case.
An UI frontend is forming messages (like "(button-clicked 'Save)" for the low level approach, or, for higher level, like "(ui-do-save)").
A backend is receiving nothing but a stream of DSL commands from the frontend (i.e., no API calls in between, the stream can even be serialised as text), and execute them one by one, replying with another stream of DSL commands for the frontend (like "(go-to-dialogue 'Choose-File)", or "(hide-save-options)").
The more abstract your command stream is, the less changes need to be propagated either way.