Here is a working example using Plugger, APILogic, and ZeroFrame, outputting things to a roll template so they can be easily broken down and understood:
!{& define ([topval] {&if {&eval}getDiceByVal($[[0]] 4 included count){&/eval} > 0}2{&else}0{&end})}{&template:default}{{name=Exceptional Hit Bonus}}{{Original Roll=[[2d4+3]]}}{{EHB=topval}}{{Final Roll=[\][\]$[[0]].value + topval\]\]}} {&simple}
Easist place to start understanding that is in the EVAL block. We're using getDiceByVal to look in roll $[[0]] for any included dice that are equal to 4, and we want to count them. So when the EVAL block finishes, we'll either have a 0, 1, or 2 (you are only rolling 2d4).
That EVAL block is in an IF block, which is going to compare that returned number against 0. If it is greater than 0, the IF will return a 2. Otherwise it will return a 0. (Incidentally, this part could be handled with Muler, if you had a more complex valuation of different results.)
That IF block is in a DEFINE block. I'm using the DEFINE block because otherwise we'd have to run the getDiceByVal plugin twice (once for the line reporting the EHB, and then once in the final roll line). If you don't need to report out the intermediate information and can get this down to a single usage of the getDiceByVal plugin, then you really don't need the DEFINE; just put the EVAL block where you need it to return the 0, 1, or 2.
(DEFINE is also a part of APILogic, so if you go to Muler but still want to use this definition-making ability to limit re-processing, you'll still require APIL.
Now we have the term topval defined to be the result of the conditional, which we can use it in the EHB line (I'm just calling this "Exceptional Hit Bonus" as a working title), and we can use it in the final roll calculation. Since we had to wait to derive that term based on the metascript processing, we have to delay the inline roll that will combine it with the original 2d4+3:
LOOP 1 =>
Roll20 => inline roll parsing (2d4+3, creating marker $[[0]]), roll template recognition
meta => EVAL plugin, Conditional APIL evaluation, and definition assignment & replacement in text of macro
(ZeroFrame stashes the roll template info, removes the roll-deferring backslashes, and sends the message back through the loop)
LOOP 2 =>
Roll20 => inline roll parsing ($[[0]] result + conditional result)
meta => no more work (ZeroFrame restores the roll template and releases the message)