Hello, I'm currently studying the implementation code for PlantUML and I do see that startsub-endsub block processing interferes with the way foreach tries to iterate, resulting in only the first pass of the foreach occurring and the loop itself being ignored. (The sub is actually trying to execute its block as its own script, but the foreach execution is still trying to loop back to its start on the original script, so its attempt isn't recognized by the sub block execution. If that makes sense.) I'm not clear on how this should be fixed, however.
I do like your example, though, as it shows a current workaround. You demonstrated that the sub block is able to execute a foreach loop that is inside a procedure it calls. I've confirmed that such a procedure can be inside the sub block itself. So, by simply wrapping your inside foreach loop in a procedure and immediately calling it, you get the desired effect -- all within the sub block.
Here is a modified version of your example, removing the outer procedure and just focusing on what happens inside the sub block, demonstrates this:
@startuml
!$data=[
{"a": 1}
,
{"a": 2}
]
!startsub test
!procedure $test_inner($data)
!foreach $d in $data
object o_interate_inside_sub.o##$d.a
!endfor
!endprocedure
$test_inner($data)
!endsub
@enduml
The results are seen here.
I've confirmed that this sub block can be imported by an external file, resulting in the expected two objects.
Wrapping the foreach inside a procedure allows its looping to work correctly inside the sub block, just requiring 3 extra lines of code (removing my whitespace). Ideally the foreach block would simply do this on its own, but for now, this is a workaround.
Regards,
JimN