To program a migration you need to write code for its execute block. The only parameter you have is m, an SModule, and what you typically want is to search for all instances of a particular deprecated concept in all models of m to migrate them.

The straightforward way to do this is the following:

for (model mdl : m.getModels()) {
  foreach node in mdl.nodes(MyDeprecatedConcept) {
    // Do something with node
  }
} 

Note the two different kinds of for loops. The for (type var : sequence) loop lets me give the iteration variable the more convenient model type instead of using the SModel type that would have been inferred by default. In the second loop I don’t need this so I can use the simpler foreach form.

There is also a second form that I have seen used in migrations. It looks like this:

with editable models in m do { 
  foreach node in #instances(MyDeprecatedConcept) { 
    // Do something with node
  } 
}

This form is slightly more flexible. In particular, it will automatically filter out non-editable models such as stubs, and you can specify whether you want to search for instances of the exact concept or include subconcepts.

I have also wondered whether the second form is more performant, but apart from the fact that it will skip non-editable models, they seem to be equivalent. The code that is generated from the two forms of course looks very differently, yet both forms use the same API in the end, FastNodeFinderManager.get(model).getNodes(concept, exact), and the non-exact search will query for subconcepts of a particular concept using ConceptDescendantsCache.getInstance().getDescendants(concept).

Skipping non-editable models will make a difference if you are going to migrate modules that contain migratable solutions together with stubs. I don’t think this makes a difference in practice so feel free to use the first form if you find it simpler to work with.