OK tested everything and it's working. Here's the latest version of the code block to add to startup.scd:
(
	~minCv = -1;
	~maxCv = 9;
	~triggerHold = 0.05;
	~latencyCorrection = -0.035;
	~dirt.orbits.do({ | orbit, i |
		var chans, cvtag, gatetag;
		chans = ~dirt.numChannels;
		// CV synths
		cvtag=('cv_np'++i).asSymbol;
		Ndef(cvtag, { | freq = 440, lag=0.01, level |
			var sig = if (
				level > 0,
				level,
				LinLin.kr( log2(freq/440), ~minCv, ~maxCv, 0, 1)
			);
			Lag.ar(K2A.ar(sig), lagTime: lag);
		});
		~dirt.soundLibrary.addSynth(('cv'++i).asSymbol, (play: {
			var latency = (~latency ? 0) + (~latencyCorrection ? 0);
			var n = ~n;
			Ndef(cvtag).wakeUp; // make sure the Ndef runs
			thisThread.clock.sched(latency, {
				Ndef(cvtag).set(\level, n);
			});
		}));
		~dirt.soundLibrary.addSynth(('cv'++i++'_pitch').asSymbol, (play: {
			var latency = (~latency ? 0) + (~latencyCorrection ? 0);
			var freq = ~freq;
			Ndef(cvtag).wakeUp; // make sure the Ndef runs
			thisThread.clock.sched(latency, {
				Ndef(cvtag).set(\level, 0, \freq, freq);
			});
		}));
		~dirt.soundLibrary.addSynth(('cv'++i++'_lag').asSymbol, (play: {
			var latency = (~latency ? 0) + (~latencyCorrection ? 0);
			var n = ~n;
			Ndef(cvtag).wakeUp; // make sure the Ndef runs
			thisThread.clock.sched(latency, {
				Ndef(cvtag).set(\lag, n);
			});
		}));
		~dirt.soundLibrary.addSynth(('cv'++i++'_on').asSymbol, (play: {
			('cv'++i++'_on').postln;
			Ndef(cvtag).play(i*chans);
		}));
		~dirt.soundLibrary.addSynth(('cv'++i++'_off').asSymbol, (play: {
			('cv'++i++'_off').postln;
			Ndef(cvtag).stop;
		}));
		// gate synths
		gatetag = ('gate_np' ++ i).asSymbol;
		Ndef(gatetag, { | level = 0, lag=0.01 |
			Lag.ar(K2A.ar(level), lagTime: lag);
		});
		~dirt.soundLibrary.addSynth(('gate'++i).asSymbol, (play: {
			var latency = (~latency ? 0) + (~latencyCorrection ? 0);
			var n = ~n;
			Ndef(gatetag).wakeUp; // make sure the Ndef runs
			thisThread.clock.sched(latency, {
				Ndef(gatetag).set(\level, n);
			});
		}));
		~dirt.soundLibrary.addSynth(('gate'++i++'_lag').asSymbol, (play: {
			var latency = (~latency ? 0) + (~latencyCorrection ? 0);
			var n = ~n;
			Ndef(gatetag).wakeUp; // make sure the Ndef runs
			thisThread.clock.sched(latency, {
				Ndef(gatetag).set(\lag, n);
			});
		}));
		~dirt.soundLibrary.addSynth(('gate'++i++'_trig').asSymbol, (play: {
			var latency = (~latency ? 0) + (~latencyCorrection ? 0);
			var triggerTime = latency + (~triggerHold ? 0.05);
			Ndef(gatetag).wakeUp; // make sure the Ndef runs
			thisThread.clock.sched(latency, {
				Ndef(gatetag).set(\level, 1.0);
			});
			thisThread.clock.sched(triggerTime, {
				Ndef(gatetag).set(\level, -0.01);
			});
		}));
		~dirt.soundLibrary.addSynth(('gate'++i++'_on').asSymbol, (play: {
			('gate'++i++'_on').postln;
			Ndef(gatetag).play(i*chans+1);
		}));
		~dirt.soundLibrary.addSynth(('gate'++i++'_off').asSymbol, (play: {
			('gate'++i++'_off').postln;
			Ndef(gatetag).stop;
		}));
	});
)
The ~minCv and ~maxCv values are used to tune the cv_pitch values for 1v/oct output.
There's a trigger synth for the gate signal that will send a short cv pulse (of ~triggerHold seconds).
Finally there's a ~latencyCorrection value that should be experimentally set to ensure the timing is correct for all these synth events.
Here's a demo of how it's used:
-- turn on cv1, this starts a continuous DC output on channel 1
once $ s "cv1_on"
-- turn on gate1
once $ s "gate1_on"
d1 $ stack [
  n "a6 a5 a6 a5" # s "cv1_pitch", 
  n "0.01 0.4" # s "cv1_lag",
  n "1 1 1 1" # s "gate1_trig"
]
And here's the output:

I'll chuck all this into a github repo and post to the opening thread soon.