Yep, you're close. So, the output of any unit generator is an array of samples representing a control rate "block." By default, the block size in Flocking is 64 samples. 

So if a unit generator is running at control rate, it will produce one sample per block. That means that only the first element in the output array will contain a value (e.g. numGrains.output[0]). If the UGen is running at audio rate, the entire block will be filled with samples. The Flocking framework will automatically determine which rate your unit generator is running at, and supply the appropriate sample count (i.e. either 1 or 64) to your gen() function.

That said, your unit generator needs to take care of adapting itself to the fact that its inputs may be running at either control or audio rates and act accordingly. Depending on the type of synthesis you're doing, some inputs only make sense to read at control rate, whereas others need to be read every sample. Or maybe it can be either, and the user will decide based on their instrument's needs.

So, for inputs that can run at both control or audio rates, the problem is that you need two different branches of code--e.g. "if the numGrains input is running at control rate, read the value at index 0 and then use that for all samples in the block I'm generating" vs. "at audio rate, I need to read the next value out of the block for every new sample I generate." In practice, it's the difference between:

var amp = that.inputs.phase.output[0];
for (var i = 0; i < numSamps; i++) {
   out[i] = value * amp;
}

and:

for (var i = 0; i < numSamps; i++) {
    out[i] = value * amp[i];
}

Clearly if, for example, the user was doing some kind of amplitude modulation synthesis, they'd be unhappy about the audio quality if you were only ready the amplitude input once per control block instead of every sample.

In SuperCollider, I've noticed that they do a lot of cutting and pasting between control vs. audio rate versions of the algorithm. I tried that approach initially, but it was way too hard to maintain. So I typically use "strides" instead. In other words, go with something closer to the second example above--I read from the input each time through my main loop. But unlike an ordinary loop, I also keep an incrementer or stride variable, which is set either to 1 or to 0 depending on the rate of the input.

Anyway, this might not make obvious sense, so maybe it's easier to point you to an example within the code base. Take a look at this and see if it is a bit clearer: https://github.com/colinbdclark/Flocking/blob/master/flocking/flocking-ugens.js#L506-L554

You can see here that, for the LFSaw unit generator, that its frequency input can run at either audio or control rate. So in the unit generator's defaults object, I list it as a "stride input." https://github.com/colinbdclark/Flocking/blob/master/flocking/flocking-ugens.js#L552

Then, whenever the input changes, I make sure to call the calculateStrides() method on my unit generator: https://github.com/colinbdclark/Flocking/blob/master/flocking/flocking-ugens.js#L538

And then, the interesting part. Within my main sample generation loop, you'll see that I actually hold two counters--one for the index of the output sample (i), and another (j) for the index of the freq input sample. This is incremented by the stride value for the input (m.strides.freq), which calculateStrides() will have automatically set  to either 0 or 1, depending on whether the frequency input is running at control or audio rate.

Whew. I hope this makes sense. If it's still fuzzy, you can drop a breakpoint in one of these unit generators and see for yourself how it works, or take my word for it and just follow the pattern.