diff --git a/ew_projects_run3/ftuple.py b/ew_projects_run3/ftuple.py
new file mode 100644
index 0000000000000000000000000000000000000000..57e2daa2a4275482bbb94a9f33478d9a2e4a77c2
--- /dev/null
+++ b/ew_projects_run3/ftuple.py
@@ -0,0 +1,129 @@
+from PyConf.reading import (get_particles, get_pvs, get_rec_summary,
+                            get_mc_particles)
+from RecoConf.event_filters import require_pvs
+from DaVinci import make_config, Options
+from DaVinci.algorithms import create_lines_filter
+from FunTuple import FunTuple_Particles as Funtuple
+from FunTuple import FunTuple_MCParticles as MCFuntuple
+from FunTuple import FunTuple_Event as EventFuntuple
+from FunTuple import FunctorCollection
+import FunTuple.functorcollections as FC
+import Functors as F
+from .ftuple_helpers import (
+    decaytree_variables, mcdecaytree_selection_to_variables, extra_event_info,
+    selection_to_decay_descriptor, mcdtt_selections_to_decay_descriptor, Spec,
+    rec_summary_variables, selection_dependent_filtering, trkeff_probes,
+    decaytreefitter_variables)
+
+hlt1_tistos_lines = ["Hlt1SingleHighPtMuon", "Hlt1SingleHighPtMuonNoMuID"]
+hlt2_tistos_lines = [
+    "Hlt2QEE_SingleHighPtMuonFull", "Hlt2QEE_SingleHighPtMuonNoMuIDFull",
+    "Hlt2QEE_ZToMuMuFull", "Hlt2QEE_ZToMuMu_SingleNoMuIDFull"
+]
+
+
+def hlt2_decision_lines(trkeff_methods):
+    return hlt2_tistos_lines + [
+        f"Hlt2TrackEff_ZToMuMu_{trkeff_method}_{probe}_{decision_type}"
+        for trkeff_method in trkeff_methods for probe in trkeff_probes
+        for decision_type in ["Tag", "Match"]
+    ]
+
+
+def make_DTT(selection, line, pvs, spec):
+    dec_desc = selection_to_decay_descriptor(
+        trkeff_methods=spec.trkeff_methods)[selection]
+    data = selection_dependent_filtering(
+        data=get_particles(f"/Event/{spec.dataLocProcess}/{line}/Particles"),
+        selection=selection)
+
+    variables = decaytree_variables(
+        selection=selection, pvs=pvs, data=data, spec=spec)
+    evtinfo = FC.EventInfo()
+    evtinfo += extra_event_info(spec=spec)
+    evtinfo += FC.SelectionInfo(
+        selection_type="Hlt1", trigger_lines=hlt1_tistos_lines)
+
+    evtinfo += rec_summary_variables(rec_summary=get_rec_summary())
+    evtinfo += FC.SelectionInfo(
+        selection_type="Hlt2",
+        trigger_lines=hlt2_decision_lines(spec.trkeff_methods))
+
+    variables["ALL"] += FC.HltTisTos(
+        selection_type="Hlt1",
+        trigger_lines=[f"{line}Decision" for line in hlt1_tistos_lines],
+        data=data)
+
+    if spec.isFull:
+        variables["ALL"] += FC.HltTisTos(
+            selection_type="Hlt2",
+            trigger_lines=[f"{line}Decision" for line in hlt2_tistos_lines],
+            data=data)
+
+    if spec.isTurCal:
+        dtf_variables = decaytreefitter_variables(
+            selection=selection, pvs=pvs, data=data)
+        for k in dtf_variables.keys():
+            variables[k] = variables.get(k, FunctorCollection(
+                {})) + dtf_variables[k]
+
+    funtuple = Funtuple(
+        name=selection,
+        tuple_name="DecayTree",
+        fields=dec_desc,
+        variables=variables,
+        inputs=data,
+        store_multiple_cand_info=True,
+        event_variables=evtinfo)
+
+    return funtuple
+
+
+def make_MCDTT(selection, spec):
+    dec_desc = mcdtt_selections_to_decay_descriptor[selection]
+    container_loc_prefix = "/Event/Spruce/HLT2" if spec.isFull else "/Event/HLT2"
+    mc_particles = get_mc_particles(f"{container_loc_prefix}/MC/Particles")
+
+    variables = mcdecaytree_selection_to_variables[selection]
+    variables["ALL"] = FunctorCollection({"ID": F.PARTICLE_ID})
+    evtinfo = extra_event_info(spec=spec)
+
+    return MCFuntuple(
+        name=f"{selection}_mcdtt",
+        tuple_name="DecayTree",
+        fields=dec_desc,
+        variables=variables,
+        event_variables=evtinfo,
+        inputs=mc_particles,
+    )
+
+
+def make_ET(spec):
+    return EventFuntuple(
+        name="EventTuple",
+        tuple_name="EventTuple",
+        variables=extra_event_info(spec=spec))
+
+
+def main(options: Options, stream: str, campaign: str, polarity: str,
+         sample: str):
+    spec = Spec(stream, campaign, polarity, sample)
+
+    algs = {}
+
+    for selection, line in spec.selection_line_map.items():
+        line_prefilter = create_lines_filter(
+            name=f"PreFilter_{line}", lines=[line])
+        pvs = get_pvs()
+        algs[selection] = [
+            line_prefilter,
+            require_pvs(pvs),
+            make_DTT(selection=selection, line=line, pvs=pvs, spec=spec)
+        ]
+    if spec.isMC: algs["EventTuple"] = [make_ET(spec=spec)]
+    if spec.isMC and spec.isFull:
+        for mcdtt_selection in mcdtt_selections_to_decay_descriptor.keys():
+            algs[f"{mcdtt_selection}_MCDecayTree"] = [
+                make_MCDTT(selection=mcdtt_selection, spec=spec)
+            ]
+    return make_config(options, algs)
diff --git a/ew_projects_run3/ftuple_helpers.py b/ew_projects_run3/ftuple_helpers.py
new file mode 100644
index 0000000000000000000000000000000000000000..89685f8139f35cdc99923cc083ebb2c220e99801
--- /dev/null
+++ b/ew_projects_run3/ftuple_helpers.py
@@ -0,0 +1,410 @@
+from PyConf.reading import get_charged_protoparticles
+from PyConf.Algorithms import WeightedRelTableAlg, ThOrParticleSelection
+from DaVinciMCTools import MCTruthAndBkgCat
+from DecayTreeFitter import DecayTreeFitter
+from Hlt2Conf.lines.qee import single_high_pt_muon
+from Hlt2Conf.standard_particles import make_ismuon_long_muon
+from Hlt2Conf.standard_jets import make_onlytrack_particleflow, make_charged_particles
+from Hlt2Conf.algorithms_thor import ParticleFilter
+from GaudiKernel.SystemOfUnits import GeV
+from FunTuple import FunctorCollection
+import Functors as F
+from enum import Enum
+
+_Z_decay_descriptor = {
+    "Z0": "^(Z0 -> mu- mu+)",
+    "mum": "Z0 -> ^mu- mu+",
+    "mup": "Z0 -> mu- ^mu+",
+}
+_Jpsi_decay_descriptor = {
+    "Jpsi": "^(J/psi(1S) -> mu- mu+)",
+    "mum": "J/psi(1S) -> ^mu- mu+",
+    "mup": "J/psi(1S) -> mu- ^mu+",
+}
+_Upsilon_decay_descriptor = {
+    "U1S": "^(Upsilon(1S) -> mu- mu+)",
+    "mum": "Upsilon(1S) -> ^mu- mu+",
+    "mup": "Upsilon(1S) -> mu- ^mu+",
+}
+_Wp_decay_descriptor = {
+    "mu": '^mu+',
+}
+_Wm_decay_descriptor = {
+    "mu": '^mu-',
+}
+trkeff_probes = ["mum", "mup"]
+
+FSR_electron_descriptor = ' {e+} {e-} {e+} {e-} '
+mcdtt_selections_to_decay_descriptor = {
+    "Z": {
+        "Z": f"^(Z0 => mu- mu+ {FSR_electron_descriptor})",
+        "mum": f"Z0 => ^mu- mu+ {FSR_electron_descriptor}",
+        "mup": f"Z0 => mu- ^mu+ {FSR_electron_descriptor}",
+    },
+    "W": {
+        "W": f"^[W+ => mu+ nu_mu {FSR_electron_descriptor}]CC",
+        "mu": f"[W+ => ^mu+ nu_mu {FSR_electron_descriptor}]CC",
+    },
+    "Ztau": {
+        "Z":
+        f"^(Z0 => (tau+ => mu+ nu_mu nu_tau~ {FSR_electron_descriptor}) (tau- => mu- nu_mu~ nu_tau {FSR_electron_descriptor}))",
+        "taup":
+        f"Z0 => ^(tau+ => mu+ nu_mu nu_tau~ {FSR_electron_descriptor}) (tau- => mu- nu_mu~ nu_tau {FSR_electron_descriptor})",
+        "mup":
+        f"Z0 => (tau+ => ^mu+ nu_mu nu_tau~ {FSR_electron_descriptor}) (tau- => mu- nu_mu~ nu_tau {FSR_electron_descriptor})",
+        "taum":
+        f"Z0 => (tau+ => mu+ nu_mu nu_tau~ {FSR_electron_descriptor}) ^(tau- => mu- nu_mu~ nu_tau {FSR_electron_descriptor})",
+        "mum":
+        f"Z0 => (tau+ => mu+ nu_mu nu_tau~ {FSR_electron_descriptor}) (tau- => ^mu- nu_mu~ nu_tau {FSR_electron_descriptor})",
+    },
+    "Wtau": {
+        "W":
+        f"^[W+ =>  (tau+ =>  mu+ nu_mu nu_tau~ {FSR_electron_descriptor})  nu_tau]CC",
+        "tau":
+        f"[W+ =>  ^(tau+ =>  mu+ nu_mu nu_tau~ {FSR_electron_descriptor})  nu_tau]CC",
+        "mu":
+        f"[W+ =>  (tau+ => ^mu+ nu_mu nu_tau~ {FSR_electron_descriptor})  nu_tau]CC",
+    }
+}
+
+_rapidity_functor = 0.5 * F.math.log((F.ENERGY + F.PZ) / (F.ENERGY - F.PZ))
+_mcdecaytree_lepton_variables = FunctorCollection({
+    "PT": F.PT,
+    "ETA": F.ETA,
+    "PHI": F.PHI
+})
+_mcdecaytree_boson_variables = FunctorCollection({
+    "M": F.MASS,
+    "PT": F.PT,
+    "Y": _rapidity_functor
+})
+mcdecaytree_selection_to_variables = {
+    "W": {
+        "mu": _mcdecaytree_lepton_variables
+    },
+    "Wtau": {
+        "mu": _mcdecaytree_lepton_variables,
+        "tau": _mcdecaytree_lepton_variables
+    },
+    "Z": {
+        "mum": _mcdecaytree_lepton_variables,
+        "mup": _mcdecaytree_lepton_variables,
+        "Z": _mcdecaytree_boson_variables,
+    },
+    "Ztau": {
+        "mum": _mcdecaytree_lepton_variables,
+        "mup": _mcdecaytree_lepton_variables,
+        "taum": _mcdecaytree_lepton_variables,
+        "taup": _mcdecaytree_lepton_variables,
+        "Z": _mcdecaytree_boson_variables,
+    },
+}
+
+
+def selection_to_decay_descriptor(trkeff_methods):
+    return {
+        "Z": _Z_decay_descriptor,
+        "ZMuID": _Z_decay_descriptor,
+        "Jpsi_Detached": _Jpsi_decay_descriptor,
+        "Jpsi_Prompt": _Jpsi_decay_descriptor,
+        "U1S": _Upsilon_decay_descriptor,
+        "Wp": _Wp_decay_descriptor,
+        "Wm": _Wm_decay_descriptor,
+        "WpNoMuID": _Wp_decay_descriptor,
+        "WmNoMuID": _Wm_decay_descriptor,
+        "ZSS": {
+            "Z0": "[Z0 -> mu- mu-]CC",
+            "mum": "[Z0 -> ^mu- mu-]CC",
+            "mup": "[Z0 -> mu- ^mu-]CC",
+        }
+    } | {
+        f"ZTrkEff_{trkeff_method}_{probe}": _Z_decay_descriptor
+        for trkeff_method in trkeff_methods for probe in trkeff_probes
+    }
+
+
+def rec_summary_variables(rec_summary):
+    # as per https://gitlab.cern.ch/lhcb/Rec/-/blob/master/Rec/RecAlgs/src/RecSummaryMaker.cpp?ref_type=heads#L71
+    _rec_summary_vars = [
+        "nPVs", "nLongTracks", "nFTClusters", "nVPClusters", "nEcalClusters",
+        "hCalTot"
+    ]
+    return FunctorCollection({
+        var: F.VALUE_OR(-1) @ F.RECSUMMARY_INFO(rec_summary, var)
+        for var in _rec_summary_vars
+    })
+
+
+def decaytree_variables(selection, pvs, spec, data):
+    variables = {}
+    variables["ALL"] = FunctorCollection({"PID": F.PARTICLE_ID})
+
+    muon_FC = FunctorCollection({
+        "BPVIP": F.BPVIP(pvs),
+        "BPVIPCHI2": F.BPVIPCHI2(pvs),
+        "TRCHI2NDOF": F.CHI2DOF,
+        "PV_X": F.BPVX(pvs),
+        "PV_Y": F.BPVY(pvs),
+        "PV_Z": F.BPVZ(pvs),
+        "Q": F.CHARGE,
+        "PX": F.PX,
+        "PY": F.PY,
+        "PZ": F.PZ,
+        "PERR2": F.PERR2(),
+        "ISMUON": F.ISMUON,
+        "INMUON": F.INMUON,
+        "PIDmu": F.PID_MU,
+        "HCALEOP": F.VALUE_OR(0) @ F.HCALEOP,
+        "ECALEOP": F.VALUE_OR(0) @ F.ELECTRONSHOWEREOP,
+    })
+    for particle in selection_to_decay_descriptor(
+            trkeff_methods=spec.trkeff_methods)[selection].keys():
+        if "mu" not in particle:  # These don't work for muons (no endvertex)
+            variables[particle] = FunctorCollection({
+                "ENDVCHI2":
+                F.CHI2 @ F.ENDVERTEX,
+                "ENDVCHI2DOF":
+                F.CHI2DOF @ F.ENDVERTEX,
+            })
+        else:
+            variables[particle] = muon_FC
+            if spec.doIsolation:
+                variables[particle] += _iso_variables(
+                    data=data, pref=particle, selection=selection)
+        if particle == "mu":  # This needs to only be Single muons (not DiMuons)
+            variables[particle] += _zveto_variables(data=data)
+
+    if spec.isMC:
+        # Necessary to prevent duplicated algorithms with different names
+        unique_selection_name = selection.replace('Wp', 'W').replace('Wm', 'W')
+        MCTRUTH = MCTruthAndBkgCat(
+            data, name=f"MCTruthAndBkgCat__{unique_selection_name}")
+        variables["ALL"] += FunctorCollection({
+            "TRUEPX":
+            MCTRUTH(F.PX),
+            "TRUEPY":
+            MCTRUTH(F.PY),
+            "TRUEPZ":
+            MCTRUTH(F.PZ),
+            "TRUEID":
+            F.VALUE_OR(0) @ MCTRUTH(F.PARTICLE_ID),
+        })
+    return variables
+
+
+def extra_event_info(spec):
+    """
+        Defines several event-level variables for later use.
+    """
+    return FunctorCollection({
+        'year_int': F.ALL() * 2024,
+        'isMC': F.ALL() * int(spec.isMC),
+        'polarity_sign': F.ALL() * spec.magpolInt
+    })
+
+
+def decaytreefitter_variables(selection, pvs, data):
+    v = {}
+    dtf__name = "DTF_AllPV"
+
+    dtf = DecayTreeFitter(
+        input_particles=data,
+        name="__".join([dtf__name, selection]),
+        input_pvs=pvs,
+        fit_all_pvs=True)
+
+    _dtf_FC = lambda d: FunctorCollection({
+        f"_{dtf__name}__{key}": dtf(functor)
+        for key, functor in d.items()
+    })
+    for muon in trkeff_probes:
+        v[muon] = _dtf_FC({
+            "P": F.P,
+            "PERR2": F.PERR2(),
+        })
+    v["Z0"] = _dtf_FC({
+        "MASS": F.MASS,
+        "CHI2DOF": F.VALUE_OR(-999) @ F.CHI2DOF,
+    })
+
+    return v
+
+
+def selection_dependent_filtering(data, selection):
+    if selection in streams_to_map()[Stream.Turbo]:
+        # https://gitlab.cern.ch/lhcb/Moore/-/merge_requests/3418#note_7953141
+        child_pidmu_cut = lambda i: F.CHILD(i, F.PID_MU) > -10
+        return ParticleFilter(
+            data,
+            F.FILTER(F.require_all(child_pidmu_cut(1), child_pidmu_cut(2))),
+            name=f'{selection}__pidmu_filter')
+    return data
+
+
+def _iso_variables(data, pref, selection):
+    """
+    For each candidate muon:
+        generate the particles within a cone with following cuts:
+            - radius 0.5 in (DPHI, DETA) space around signal candidate
+            - ~SHARE_TRACKS i.e. don't count our signal candidate
+        Defines:
+          ISO_C{PT,PX,PY} via _safe_sumcone
+    """
+    # extract muon particlecontainer from the main decay candidate.
+    mu_decdesc = {"mup": "mu+", "mum": "mu-", "mu": "mu"}[pref]
+    isDimuon = "W" not in selection
+    mu_particles = _extract_muons_from_composite_candidate(
+        data=data, decdesc=mu_decdesc) if isDimuon else data
+
+    with make_charged_particles.bind(
+            make_protoparticles=get_charged_protoparticles):
+        input_cands = make_onlytrack_particleflow()
+    alg = WeightedRelTableAlg(
+        ReferenceParticles=mu_particles,
+        InputCandidates=input_cands,
+        Cut=F.require_all(F.DR2 < (0.5**2), ~F.SHARE_TRACKS()))
+    return FunctorCollection({
+        "ISO050_CPT": _safe_sumcone(alg, F.PT),
+        "ISO050_CPX": _safe_sumcone(alg, F.PX),
+        "ISO050_CPY": _safe_sumcone(alg, F.PY),
+    })
+
+
+def _zveto_variables(data):
+    """
+       Only makes sense for W-like candidates, i.e. not Z-like.
+    """
+    min_muon_pt = 0.1 * GeV  # Just a sanity cut
+    input_cands = ParticleFilter(
+        make_ismuon_long_muon(),
+        F.FILTER(F.require_all(F.PT > min_muon_pt)),
+        name='ZVeto_muon_filter')
+
+    alg = WeightedRelTableAlg(
+        ReferenceParticles=data,
+        InputCandidates=input_cands,
+        Cut=F.require_all(~F.SHARE_TRACKS()))
+
+    zveto_muon_variables = {"PX": F.PX, "PY": F.PY, "PZ": F.PZ, "Q": F.CHARGE}
+
+    def _zveto_muon_functor_composition(functor, relations):
+        return F.VALUE_OR(0) @ functor @ F.TO @ F.ENTRY_WITH_MAX_REL_VALUE_OF(
+            F.PT @ F.TO @ F.FORWARDARG0).bind(
+                F.RELATIONS.bind(F.TES(relations), F.FORWARDARGS))
+
+    return FunctorCollection({
+        f"OTHERMUON_{var}": _zveto_muon_functor_composition(
+            functor=functor, relations=alg.OutputRelations)
+        for var, functor in zveto_muon_variables.items()
+    })
+
+
+def _extract_muons_from_composite_candidate(data, decdesc):
+    """
+        Navigates the descendants of a candidate particle
+        Returns the child muons for that candidate.
+    """
+    return ThOrParticleSelection(
+        InputParticles=data,
+        Functor=F.FILTER(F.IS_ID(decdesc))
+        @ F.GET_ALL_DESCENDANTS()).OutputSelection
+
+
+def _safe_sumcone(reltable_alg, functor):
+    """
+        Given a relation table and a functor:
+            Returns SUM(functor) over all particles within that relation table.
+        Safe refers to giving the physical invalid value of 0 if the relation is empty (i.e. instead of NaN)
+    """
+    return F.VALUE_OR(0) @ F.SUMCONE(
+        Functor=functor, Relations=reltable_alg.OutputRelations)
+
+
+class Sample(Enum):
+    MC = 0
+    DATA = 1
+
+
+class Campaign(Enum):
+    s24c1 = 1
+    s24c2 = 2
+    s24c3 = 3
+
+
+class Stream(Enum):
+    Full = 1
+    Turbo = 2
+    TurCal = 3
+
+
+class MagPol(Enum):
+    MagUp = 1
+    MagDown = -1
+
+
+def _data_loc_process(stream_enum):
+    return {
+        Stream.Full: 'Spruce',
+        Stream.Turbo: 'HLT2',
+        Stream.TurCal: 'Spruce'
+    }[stream_enum]
+
+
+def _do_isolation(stream_enum):
+    return {
+        Stream.Full: True,
+        Stream.Turbo: False,
+        Stream.TurCal: True
+    }[stream_enum]
+
+
+def _trkeff_methods(campaign_enum):
+    m_withoutUT = ["VeloMuon", "SeedMuon"]
+    m_withUT = m_withoutUT + ["MuonUT", "Downstream"]
+    return {
+        Campaign.s24c1: m_withoutUT,
+        Campaign.s24c2: m_withUT,
+        Campaign.s24c3: m_withUT
+    }[campaign_enum]
+
+
+def streams_to_map(trkeff_methods=[""]):
+    return {  # Stream : { selection : linename }
+        Stream.Turbo: {
+            "Jpsi_Detached": "Hlt2QEE_JpsiToMuMu_Detached",
+            "Jpsi_Prompt": "Hlt2QEE_JpsiToMuMu_Prompt",
+            "U1S": "Hlt2QEE_Upsilon1SToMuMu",
+        },
+        Stream.Full: {
+            "Z": "SpruceQEE_ZToMuMu",
+            "ZMuID": "SpruceQEE_ZToMuMu_SingleNoMuID",
+            "Wp": "SpruceQEE_SingleHighPtMuon",
+            "Wm": "SpruceQEE_SingleHighPtMuon",
+            "WpNoMuID": "SpruceQEE_SingleHighPtMuonNoMuID",
+            "WmNoMuID": "SpruceQEE_SingleHighPtMuonNoMuID",
+            "ZSS": "SpruceQEE_DiMuonSameSign",
+        },
+        Stream.TurCal: {
+            f"ZTrkEff_{trkeff_method}_{probe}":
+            f"SpruceTurCalTrackEff_ZToMuMu_{trkeff_method}_{probe}_Tag"
+            for trkeff_method in trkeff_methods for probe in trkeff_probes
+        }
+    }
+
+
+class Spec():
+    def __init__(self, stream, campaign, magpol, sample):
+        stream_enum = Stream[stream]
+        campaign_enum = Campaign[f's{campaign}']
+        magpol_enum = MagPol[magpol]
+        sample_enum = Sample[sample]
+
+        self.isFull = stream_enum == Stream.Full
+        self.isTurCal = stream_enum == Stream.TurCal
+        self.isMC = sample_enum == Sample.MC
+        self.magpolInt = magpol_enum.value
+        self.trkeff_methods = _trkeff_methods(campaign_enum)
+        self.selection_line_map = streams_to_map(
+            self.trkeff_methods)[stream_enum]
+        self.dataLocProcess = _data_loc_process(stream_enum)
+        self.doIsolation = _do_isolation(stream_enum)
diff --git a/ew_projects_run3/info.yaml b/ew_projects_run3/info.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..0858620485fa14a7edd09eaed26f585318ae6e88
--- /dev/null
+++ b/ew_projects_run3/info.yaml
@@ -0,0 +1,58 @@
+defaults:
+  inform:
+    - luke.grazette@cern.ch
+  wg: QEE
+
+################
+##### DATA #####
+################
+
+
+# https://twiki.cern.ch/twiki/bin/viewauth/LHCbInternal/OperationSummary2024
+# sprucing_campaign, moore_v, tuple_v
+# 24c1 : Moore/DV latest `2024-patches` release as of 22-Apr-24
+# 24c2 : started withUT ~JuneTS : https://gitlab.cern.ch/lhcb-core/lhcbstacks/-/blob/master/data/stacks/DPA/2024.06.18.yml
+# 24c3 : Started post AugMD
+{%- set processing_campaigns = [
+  ('24c3', '/Real Data/Sprucing24c3', 'v55r12p3', 'v64r9')
+]%}
+{%- set polarities=['MagDown', 'MagUp'] %}
+
+{%- for campaign, campaign_bkkpath, moore_v, tuple_v in processing_campaigns %}
+  {%- for polarity in polarities %}
+    {%- set bkk_prefix = '/LHCb/Collision24/Beam6800GeV-VeloClosed-' + polarity + campaign_bkkpath %}
+    {%- set datasets = [
+        ('TurCal', 'Spruce'   , 'turcal_persistrecorawbanks', bkk_prefix + '/95100000/TURCAL_PERSISTRECORAWBANKS.DST'),
+    ]
+    %}
+    {%- for stream, input_process, input_stream, bk_query in datasets %}
+
+DATA{{campaign}}_{{polarity}}_{{stream}}:
+  application: DaVinci/{{tuple_v}}
+  input:
+    bk_query: {{bk_query}}
+    dq_flags:
+      - OK
+      - UNCHECKED
+  output: FTupleQEE.root
+  options:
+    entrypoint: ew_projects_run3.ftuple:main
+    extra_options:
+      input_raw_format: 0.5
+      input_type: ROOT
+      simulation: False
+      data_type: "Upgrade"
+      geometry_version: run3/2024.Q1.2-v00.00
+      conditions_version: master
+      input_process: {{input_process}}
+      input_stream: {{input_stream}}
+    extra_args:
+      - --
+      - {{stream}}
+      - {{campaign}}
+      - {{polarity}}
+      - DATA
+
+    {%- endfor %}
+  {%- endfor %}
+{%- endfor %}
diff --git a/ew_projects_run3/mc/24c1/hlt1.py b/ew_projects_run3/mc/24c1/hlt1.py
new file mode 100644
index 0000000000000000000000000000000000000000..3e376e91fdb6821818bc7c777a9e04ecd48c38bc
--- /dev/null
+++ b/ew_projects_run3/mc/24c1/hlt1.py
@@ -0,0 +1,14 @@
+"""
+Stolen verbatim from Hlt/Hlt1Conf/options/hlt1_pp_expected_24_without_UT.py from v55r7
+ - Added a `make_passthrough_line` bind
+"""
+from Moore import Options
+from Moore.config import run_allen
+from RecoConf.hlt1_allen import allen_gaudi_config as allen_sequence
+from AllenConf.hlt1_calibration_lines import make_passthrough_line
+
+
+def main(options: Options):
+    with allen_sequence.bind(sequence="hlt1_pp_matching_no_ut_1000KHz"),\
+         make_passthrough_line.bind(pre_scaler=1):
+        return run_allen(options)
diff --git a/ew_projects_run3/mc/24c1/hlt2.py b/ew_projects_run3/mc/24c1/hlt2.py
new file mode 100644
index 0000000000000000000000000000000000000000..3c014a60f63dee0c4a89f5a98030c0bcc502ef06
--- /dev/null
+++ b/ew_projects_run3/mc/24c1/hlt2.py
@@ -0,0 +1,92 @@
+"""
+Stolen and adapted from Hlt/Hlt2Conf/options/hlt2_pp_expected_24_without_UT.py from v55r7.
+"""
+from Moore import Options, run_moore
+from RecoConf.reconstruction_objects import reconstruction
+from RecoConf.hlt2_global_reco import reconstruction as hlt2_reconstruction, make_light_reco_pr_kf_without_UT
+from RecoConf.decoders import default_VeloCluster_source
+from RecoConf.global_tools import stateProvider_with_simplified_geom, trackMasterExtrapolator_with_simplified_geom
+from RecoConf.hlt2_tracking import (
+    make_TrackBestTrackCreator_tracks,
+    make_PrKalmanFilter_noUT_tracks,
+    make_PrKalmanFilter_Velo_tracks,
+    make_PrKalmanFilter_Seed_tracks,
+)
+from RecoConf.event_filters import require_gec
+from RecoConf.protoparticles import make_charged_protoparticles
+from Hlt2Conf.settings.defaults import get_default_hlt1_filter_code_for_hlt2
+
+from Moore.streams import Stream, Streams, DETECTORS
+import sys
+from Moore.lines import Hlt2Line
+from Hlt2Conf.lines.qee.high_mass_dimuon import all_lines as qee_dimuon_lines
+from Hlt2Conf.lines.qee.single_high_pt_muon import all_lines as qee_single_muon_lines
+from Hlt2Conf.lines.qee.single_high_pt_electron import all_lines as qee_single_electron_lines
+from Hlt2Conf.lines.qee.quarkonia import all_lines as qee_quarkonia_lines
+from Hlt2Conf.lines.trackeff.ZTrackEfficiency import all_lines as z_trackeff_lines
+
+
+def pass_through_line(name="Hlt2Passthrough"):
+    """Return a HLT2 line that performs no selection but runs and persists the reconstruction
+    """
+    return Hlt2Line(name=name, prescale=1, algs=[], persistreco=True)
+
+
+def _pop_line(linename, line_dict):
+    print(f"Removing {linename}")
+    line_dict.pop(linename)
+    return 0
+
+
+def _make_lines():
+    # Remove UT dependent lines
+    for linename in [
+            name for name in z_trackeff_lines.keys()
+            if "Downstream" in name or "MuonUT" in name
+    ]:
+        _pop_line(linename, z_trackeff_lines)
+
+    ret = [
+        builder(prescale=1) for line_dict in [
+            qee_single_muon_lines, qee_dimuon_lines, qee_quarkonia_lines,
+            qee_single_electron_lines, z_trackeff_lines
+        ] for builder in line_dict.values()
+    ]
+    ret += [pass_through_line()]
+    return ret
+
+
+def make_streams():
+    streams = [
+        Stream(lines=_make_lines(), routing_bit=98, detectors=DETECTORS)
+    ]
+    return Streams(streams=streams)
+
+
+def main(options: Options):
+    # NOTE: These are fr from options/hlt2_pp_2024
+    from PyConf.Tools import TrackMasterExtrapolator, TrackMasterFitter
+    TrackMasterExtrapolator.global_bind(
+        ApplyMultScattCorr=False,
+        ApplyEnergyLossCorr=False,
+        ApplyElectronEnergyLossCorr=False)
+    TrackMasterFitter.global_bind(ApplyMaterialCorrections=False)
+
+    public_tools = [
+        trackMasterExtrapolator_with_simplified_geom(),
+        stateProvider_with_simplified_geom()
+    ]
+
+    # NOTE: the TBTC does not apply a chi2 cut because it is applied by the PrKF
+    # NOTE: reconstruction and default VeloCluster are from options/hlt2_pp_2024
+    with reconstruction.bind(from_file=False),\
+         default_VeloCluster_source.bind(bank_type="VPRetinaCluster"),\
+         hlt2_reconstruction.bind(make_reconstruction=make_light_reco_pr_kf_without_UT),\
+         require_gec.bind(skipUT=True),\
+         make_charged_protoparticles.bind(fill_probnn_defaults=True),\
+         make_TrackBestTrackCreator_tracks.bind(max_ghost_prob=0.7, max_chi2ndof=sys.float_info.max),\
+         make_PrKalmanFilter_Velo_tracks.bind(max_chi2ndof=5.),\
+         make_PrKalmanFilter_noUT_tracks.bind(max_chi2ndof=4.),\
+         make_PrKalmanFilter_Seed_tracks.bind(max_chi2ndof=6.),\
+         get_default_hlt1_filter_code_for_hlt2.bind(code=r"Hlt1(?!PassthroughLargeEvent).*Decision"):
+        return run_moore(options, make_streams, public_tools=public_tools)
diff --git a/ew_projects_run3/mc/24c1/spruce.py b/ew_projects_run3/mc/24c1/spruce.py
new file mode 100644
index 0000000000000000000000000000000000000000..336b224a4c5fc000cbb08d21f1b04c6b52c40be3
--- /dev/null
+++ b/ew_projects_run3/mc/24c1/spruce.py
@@ -0,0 +1,87 @@
+"""
+The different streaming configurations stolen from the following on v55r7:
+    Turbo: Hlt/Hlt2Conf/options/sprucing/sprucepass_pp_expected_24.py
+    Full: Hlt/Hlt2Conf/options/sprucing/spruce_pp_expected_24.py
+    TurCal: Hlt/Hlt2Conf/python/Hlt2Conf/sprucing_settings/Sprucing_production_PP_24c1.py
+    Removed reco binds as per https://gitlab.cern.ch/lhcb-datapkg/AnalysisProductions/-/merge_requests/1119#note_7859546
+"""
+from Moore import Options, run_moore
+from Moore.streams import DETECTORS, Stream, Streams
+from Moore.lines import PassLine, SpruceLine
+from PyConf.reading import get_particles, upfront_reconstruction as upfront_spruce
+from RecoConf.global_tools import stateProvider_with_simplified_geom
+
+from Hlt2Conf.lines.qee import sprucing_lines as qee_sprucing_lines
+
+from Moore.persistence.hlt2_tistos import list_of_full_stream_lines
+from Hlt2Conf.lines.qee.high_mass_dimuon import all_lines as qee_dimuon_lines
+from Hlt2Conf.lines.qee.single_high_pt_muon import all_lines as qee_single_muon_lines
+from Hlt2Conf.lines.trackeff.ZTrackEfficiency import all_lines as z_trackeff_lines
+
+
+def make_turbo_streams():
+    streams = [Stream(lines=[PassLine(name="PassLine")], detectors=DETECTORS)]
+    return Streams(streams=streams)
+
+
+def pass_through_line(name="SprucePassThrough"):
+    """Return a Spruce line that performs no selection but runs and persists the reconstruction
+    """
+    return SpruceLine(
+        name=name, prescale=1, algs=upfront_spruce(), persistreco=True)
+
+
+def make_full_streams():
+    lines = [builder() for builder in qee_sprucing_lines.values()]
+    lines += [pass_through_line(name="SprucePassThrough__exclusive")]
+    streams = [Stream(lines=lines, detectors=[])]
+    return Streams(streams=streams)
+
+
+def make_turcal_spruceline(hlt2_linename, persist_reco=False, prescale=1.):
+    filter = f"{hlt2_linename}Decision"
+    location = f"/Event/HLT2/{hlt2_linename}/Particles"
+    spruce_linename = hlt2_linename.replace("Hlt2", "SpruceTurCal")
+    print(f"{filter} {location} {spruce_linename}")
+    hlt2_particles = get_particles(location)
+    turcal_spruce_line = SpruceLine(
+        name=spruce_linename,
+        hlt2_filter_code=filter,
+        algs=upfront_spruce() + [hlt2_particles],
+        persistreco=persist_reco,
+        prescale=prescale)
+    return turcal_spruce_line
+
+
+def make_turcal_streams():
+    # Remove UT dependent lines
+    lines = [
+        make_turcal_spruceline(linename, persist_reco=True)
+        for linename in z_trackeff_lines.keys()
+        if ("Downstream" not in linename and "MuonUT" not in linename)
+    ]
+    lines += [pass_through_line(name="SprucePassThrough__turcal")]
+    streams = [Stream(lines=lines, detectors=DETECTORS)]
+    return Streams(streams=streams)
+
+
+def lines_for_tistos():
+    return [
+        linename for line_dict in [qee_single_muon_lines, qee_dimuon_lines]
+        for linename in line_dict.keys()
+    ]
+
+
+def Turbo(options: Options):
+    return run_moore(options, make_turbo_streams, public_tools=[])
+
+
+def Full(options: Options):
+    public_tools = [stateProvider_with_simplified_geom()]
+    with list_of_full_stream_lines.bind(lines=lines_for_tistos()):
+        return run_moore(options, make_full_streams, public_tools=public_tools)
+
+
+def TurCal(options: Options):
+    with list_of_full_stream_lines.bind(lines=[]):
+        return run_moore(options, make_turcal_streams, public_tools=[])
diff --git a/ew_projects_run3/mc/24c2/hlt1.py b/ew_projects_run3/mc/24c2/hlt1.py
new file mode 100644
index 0000000000000000000000000000000000000000..07fc116dcf6b008eef7b16260f71359cd11745df
--- /dev/null
+++ b/ew_projects_run3/mc/24c2/hlt1.py
@@ -0,0 +1,14 @@
+"""
+Stolen verbatim from Hlt/Hlt1Conf/options/hlt1_pp_expected_24_without_UT.py from v55r10p1
+ - Added a `make_passthrough_line` bind
+"""
+from Moore import Options
+from Moore.config import run_allen
+from RecoConf.hlt1_allen import allen_gaudi_config as allen_sequence
+from AllenConf.hlt1_calibration_lines import make_passthrough_line
+
+
+def main(options: Options):
+    with allen_sequence.bind(sequence="hlt1_pp_matching_no_ut_1000KHz"),\
+         make_passthrough_line.bind(pre_scaler=1):
+        return run_allen(options)
diff --git a/ew_projects_run3/mc/24c2/hlt2.py b/ew_projects_run3/mc/24c2/hlt2.py
new file mode 100644
index 0000000000000000000000000000000000000000..fc22f15abfb0d497a3485d83375536d5e7117a66
--- /dev/null
+++ b/ew_projects_run3/mc/24c2/hlt2.py
@@ -0,0 +1,73 @@
+from Moore import Options, run_moore
+from RecoConf.reconstruction_objects import reconstruction
+from RecoConf.decoders import default_VeloCluster_source
+from RecoConf.global_tools import stateProvider_with_simplified_geom, trackMasterExtrapolator_with_simplified_geom
+
+from RecoConf.hlt2_global_reco import (
+    reconstruction as hlt2_reconstruction,
+    make_light_reco_pr_kf,
+)
+from RecoConf.hlt2_tracking import (
+    make_TrackBestTrackCreator_tracks,
+    make_PrKalmanFilter_tracks,
+    make_PrKalmanFilter_Velo_tracks,
+    make_PrKalmanFilter_Seed_tracks,
+    make_PrHybridSeeding_tracks,
+)
+from RecoConf.event_filters import require_gec
+from Hlt2Conf.settings.defaults import get_default_hlt1_filter_code_for_hlt2
+
+import sys
+
+from Moore.lines import Hlt2Line
+from Moore.streams import Stream, Streams, DETECTORS
+from Hlt2Conf.lines.qee.high_mass_dimuon import all_lines as qee_dimuon_lines
+from Hlt2Conf.lines.qee.single_high_pt_muon import all_lines as qee_single_muon_lines
+from Hlt2Conf.lines.qee.single_high_pt_electron import all_lines as qee_single_electron_lines
+from Hlt2Conf.lines.qee.quarkonia import all_lines as qee_quarkonia_lines
+from Hlt2Conf.lines.trackeff.ZTrackEfficiency import all_lines as z_trackeff_lines
+
+
+def pass_through_line(name="Hlt2Passthrough"):
+    """Return a HLT2 line that performs no selection but runs and persists the reconstruction
+    """
+    return Hlt2Line(name=name, prescale=1, algs=[], persistreco=True)
+
+
+def _make_lines():
+    ret = [
+        builder(prescale=1) for line_dict in [
+            qee_single_muon_lines, qee_dimuon_lines, qee_quarkonia_lines,
+            qee_single_electron_lines, z_trackeff_lines
+        ] for builder in line_dict.values()
+    ]
+    ret += [pass_through_line()]
+    return ret
+
+
+def make_streams():
+    streams = [
+        Stream(lines=_make_lines(), routing_bit=98, detectors=DETECTORS)
+    ]
+    return Streams(streams=streams)
+
+
+def main(options: Options):
+    # https://gitlab.cern.ch/lhcb/Moore/-/blob/v55r10p1/Hlt/Hlt2Conf/python/Hlt2Conf/settings/hlt2_pp_2024.py
+    # https://gitlab.cern.ch/lhcb/Moore/-/blob/v55r10p1/Hlt/Hlt2Conf/options/hlt2_pp_2024.py
+    public_tools = [
+        trackMasterExtrapolator_with_simplified_geom(),
+        stateProvider_with_simplified_geom()
+    ]
+    with reconstruction.bind(from_file=False),\
+         default_VeloCluster_source.bind(bank_type="VPRetinaCluster"),\
+         hlt2_reconstruction.bind(make_reconstruction=make_light_reco_pr_kf),\
+         require_gec.bind(skipUT=True),\
+         make_TrackBestTrackCreator_tracks.bind(max_ghost_prob=0.5, max_chi2ndof=sys.float_info.max),\
+         make_PrKalmanFilter_Velo_tracks.bind(max_chi2ndof=5.),\
+         make_PrKalmanFilter_tracks.bind(max_chi2ndof=4.),\
+         make_PrKalmanFilter_Seed_tracks.bind(max_chi2ndof=6.),\
+         get_default_hlt1_filter_code_for_hlt2.bind(code=r"Hlt1(?!PassthroughLargeEvent).*Decision"),\
+         make_PrHybridSeeding_tracks.bind(FasterCloneRemoval=True):
+
+        return run_moore(options, make_streams, public_tools=public_tools)
diff --git a/ew_projects_run3/mc/24c2/spruce.py b/ew_projects_run3/mc/24c2/spruce.py
new file mode 100644
index 0000000000000000000000000000000000000000..6aab0f546528e532851965bb0765ea75771d85a7
--- /dev/null
+++ b/ew_projects_run3/mc/24c2/spruce.py
@@ -0,0 +1,87 @@
+from Moore import Options, run_moore
+from Moore.streams import DETECTORS, Stream, Streams
+from Moore.lines import PassLine, SpruceLine
+
+from PyConf.reading import get_particles, upfront_reconstruction as upfront_spruce
+from Moore.persistence.hlt2_tistos import list_of_full_stream_lines
+
+from RecoConf.global_tools import stateProvider_with_simplified_geom, trackMasterExtrapolator_with_simplified_geom
+from PyConf.Algorithms import VeloRetinaClusterTrackingSIMD
+from RecoConf.legacy_rec_hlt1_tracking import make_VeloClusterTrackingSIMD
+from RecoConf.decoders import default_VeloCluster_source
+
+from Hlt2Conf.lines.qee import sprucing_lines as qee_sprucing_lines
+from Hlt2Conf.lines.qee.high_mass_dimuon import all_lines as qee_dimuon_lines
+from Hlt2Conf.lines.qee.single_high_pt_muon import all_lines as qee_single_muon_lines
+from Hlt2Conf.lines.trackeff.ZTrackEfficiency import all_lines as z_trackeff_lines
+
+# https://gitlab.cern.ch/lhcb-datapkg/SprucingConfig/-/blob/master/SprucingConfig/Spruce24/Sprucing_production_physics_pp_Collision24c2.py
+# Removed reco binds as per https://gitlab.cern.ch/lhcb-datapkg/AnalysisProductions/-/merge_requests/1119#note_7859546
+
+
+def pass_through_line(name="SprucePassThrough"):
+    """Return a Spruce line that performs no selection but runs and persists the reconstruction
+    """
+    return SpruceLine(
+        name=name, prescale=1, algs=upfront_spruce(), persistreco=True)
+
+
+def Full(options: Options):
+    def _make_streams():
+        lines = [builder() for builder in qee_sprucing_lines.values()]
+        lines += [pass_through_line(name="SprucePassThrough__exclusive")]
+        streams = [Stream(lines=lines, detectors=[])]
+        return Streams(streams=streams)
+
+    default_VeloCluster_source.global_bind(bank_type="VPRetinaCluster")
+    make_VeloClusterTrackingSIMD.global_bind(
+        algorithm=VeloRetinaClusterTrackingSIMD)
+
+    public_tools = [
+        trackMasterExtrapolator_with_simplified_geom(),
+        stateProvider_with_simplified_geom()
+    ]
+    full_lines_for_TISTOS = [
+        linename for line_dict in [qee_single_muon_lines, qee_dimuon_lines]
+        for linename in line_dict.keys()
+    ]
+    with list_of_full_stream_lines.bind(lines=full_lines_for_TISTOS):
+        return run_moore(options, _make_streams, public_tools=public_tools)
+
+
+def Turbo(options: Options):
+    def _make_streams():
+        streams = [
+            Stream(lines=[PassLine(name="PassLine")], detectors=DETECTORS)
+        ]
+        return Streams(streams=streams)
+
+    return run_moore(options, _make_streams, public_tools=[])
+
+
+def TurCal(options: Options):
+    def make_turcal_spruceline(hlt2_linename, persistreco=False, prescale=1.):
+        line_filter = f"{hlt2_linename}Decision"
+        location = f"/Event/HLT2/{hlt2_linename}/Particles"
+        spruce_linename = hlt2_linename.replace("Hlt2", "SpruceTurCal")
+        print(f"{line_filter} {location} {spruce_linename}")
+        hlt2_particles = get_particles(location)
+        turcal_spruce_line = SpruceLine(
+            name=spruce_linename,
+            hlt2_filter_code=line_filter,
+            algs=[hlt2_particles],
+            persistreco=persistreco,
+            prescale=prescale)
+        return turcal_spruce_line
+
+    def _make_streams():
+        lines = [
+            make_turcal_spruceline(linename, persistreco=True)
+            for linename in z_trackeff_lines.keys()
+        ]
+        lines += [pass_through_line(name="SprucePassThrough__turcal")]
+        streams = [Stream(lines=lines, detectors=DETECTORS)]
+        return Streams(streams=streams)
+
+    with list_of_full_stream_lines.bind(lines=[]):
+        return run_moore(options, _make_streams, public_tools=[])
diff --git a/ew_projects_run3/mc/24c3/hlt1.py b/ew_projects_run3/mc/24c3/hlt1.py
new file mode 100644
index 0000000000000000000000000000000000000000..5cee9ba1671503ff65922091af1442bc25a9b81f
--- /dev/null
+++ b/ew_projects_run3/mc/24c3/hlt1.py
@@ -0,0 +1,14 @@
+"""
+Stolen verbatim from Hlt/Hlt1Conf/options/hlt1_pp_expected_24_without_UT.py from v55r10p1
+ - Added a `make_passthrough_line` bind
+"""
+from Moore import Options
+from Moore.config import run_allen
+from RecoConf.hlt1_allen import allen_gaudi_config as allen_sequence
+from AllenConf.hlt1_calibration_lines import make_passthrough_line
+
+
+def main(options: Options):
+    with allen_sequence.bind(sequence="hlt1_pp_forward_then_matching_1000kHz"),\
+         make_passthrough_line.bind(pre_scaler=1):
+        return run_allen(options)
diff --git a/ew_projects_run3/mc/24c3/hlt2.py b/ew_projects_run3/mc/24c3/hlt2.py
new file mode 100644
index 0000000000000000000000000000000000000000..fc22f15abfb0d497a3485d83375536d5e7117a66
--- /dev/null
+++ b/ew_projects_run3/mc/24c3/hlt2.py
@@ -0,0 +1,73 @@
+from Moore import Options, run_moore
+from RecoConf.reconstruction_objects import reconstruction
+from RecoConf.decoders import default_VeloCluster_source
+from RecoConf.global_tools import stateProvider_with_simplified_geom, trackMasterExtrapolator_with_simplified_geom
+
+from RecoConf.hlt2_global_reco import (
+    reconstruction as hlt2_reconstruction,
+    make_light_reco_pr_kf,
+)
+from RecoConf.hlt2_tracking import (
+    make_TrackBestTrackCreator_tracks,
+    make_PrKalmanFilter_tracks,
+    make_PrKalmanFilter_Velo_tracks,
+    make_PrKalmanFilter_Seed_tracks,
+    make_PrHybridSeeding_tracks,
+)
+from RecoConf.event_filters import require_gec
+from Hlt2Conf.settings.defaults import get_default_hlt1_filter_code_for_hlt2
+
+import sys
+
+from Moore.lines import Hlt2Line
+from Moore.streams import Stream, Streams, DETECTORS
+from Hlt2Conf.lines.qee.high_mass_dimuon import all_lines as qee_dimuon_lines
+from Hlt2Conf.lines.qee.single_high_pt_muon import all_lines as qee_single_muon_lines
+from Hlt2Conf.lines.qee.single_high_pt_electron import all_lines as qee_single_electron_lines
+from Hlt2Conf.lines.qee.quarkonia import all_lines as qee_quarkonia_lines
+from Hlt2Conf.lines.trackeff.ZTrackEfficiency import all_lines as z_trackeff_lines
+
+
+def pass_through_line(name="Hlt2Passthrough"):
+    """Return a HLT2 line that performs no selection but runs and persists the reconstruction
+    """
+    return Hlt2Line(name=name, prescale=1, algs=[], persistreco=True)
+
+
+def _make_lines():
+    ret = [
+        builder(prescale=1) for line_dict in [
+            qee_single_muon_lines, qee_dimuon_lines, qee_quarkonia_lines,
+            qee_single_electron_lines, z_trackeff_lines
+        ] for builder in line_dict.values()
+    ]
+    ret += [pass_through_line()]
+    return ret
+
+
+def make_streams():
+    streams = [
+        Stream(lines=_make_lines(), routing_bit=98, detectors=DETECTORS)
+    ]
+    return Streams(streams=streams)
+
+
+def main(options: Options):
+    # https://gitlab.cern.ch/lhcb/Moore/-/blob/v55r10p1/Hlt/Hlt2Conf/python/Hlt2Conf/settings/hlt2_pp_2024.py
+    # https://gitlab.cern.ch/lhcb/Moore/-/blob/v55r10p1/Hlt/Hlt2Conf/options/hlt2_pp_2024.py
+    public_tools = [
+        trackMasterExtrapolator_with_simplified_geom(),
+        stateProvider_with_simplified_geom()
+    ]
+    with reconstruction.bind(from_file=False),\
+         default_VeloCluster_source.bind(bank_type="VPRetinaCluster"),\
+         hlt2_reconstruction.bind(make_reconstruction=make_light_reco_pr_kf),\
+         require_gec.bind(skipUT=True),\
+         make_TrackBestTrackCreator_tracks.bind(max_ghost_prob=0.5, max_chi2ndof=sys.float_info.max),\
+         make_PrKalmanFilter_Velo_tracks.bind(max_chi2ndof=5.),\
+         make_PrKalmanFilter_tracks.bind(max_chi2ndof=4.),\
+         make_PrKalmanFilter_Seed_tracks.bind(max_chi2ndof=6.),\
+         get_default_hlt1_filter_code_for_hlt2.bind(code=r"Hlt1(?!PassthroughLargeEvent).*Decision"),\
+         make_PrHybridSeeding_tracks.bind(FasterCloneRemoval=True):
+
+        return run_moore(options, make_streams, public_tools=public_tools)
diff --git a/ew_projects_run3/mc/24c3/spruce.py b/ew_projects_run3/mc/24c3/spruce.py
new file mode 100644
index 0000000000000000000000000000000000000000..6aab0f546528e532851965bb0765ea75771d85a7
--- /dev/null
+++ b/ew_projects_run3/mc/24c3/spruce.py
@@ -0,0 +1,87 @@
+from Moore import Options, run_moore
+from Moore.streams import DETECTORS, Stream, Streams
+from Moore.lines import PassLine, SpruceLine
+
+from PyConf.reading import get_particles, upfront_reconstruction as upfront_spruce
+from Moore.persistence.hlt2_tistos import list_of_full_stream_lines
+
+from RecoConf.global_tools import stateProvider_with_simplified_geom, trackMasterExtrapolator_with_simplified_geom
+from PyConf.Algorithms import VeloRetinaClusterTrackingSIMD
+from RecoConf.legacy_rec_hlt1_tracking import make_VeloClusterTrackingSIMD
+from RecoConf.decoders import default_VeloCluster_source
+
+from Hlt2Conf.lines.qee import sprucing_lines as qee_sprucing_lines
+from Hlt2Conf.lines.qee.high_mass_dimuon import all_lines as qee_dimuon_lines
+from Hlt2Conf.lines.qee.single_high_pt_muon import all_lines as qee_single_muon_lines
+from Hlt2Conf.lines.trackeff.ZTrackEfficiency import all_lines as z_trackeff_lines
+
+# https://gitlab.cern.ch/lhcb-datapkg/SprucingConfig/-/blob/master/SprucingConfig/Spruce24/Sprucing_production_physics_pp_Collision24c2.py
+# Removed reco binds as per https://gitlab.cern.ch/lhcb-datapkg/AnalysisProductions/-/merge_requests/1119#note_7859546
+
+
+def pass_through_line(name="SprucePassThrough"):
+    """Return a Spruce line that performs no selection but runs and persists the reconstruction
+    """
+    return SpruceLine(
+        name=name, prescale=1, algs=upfront_spruce(), persistreco=True)
+
+
+def Full(options: Options):
+    def _make_streams():
+        lines = [builder() for builder in qee_sprucing_lines.values()]
+        lines += [pass_through_line(name="SprucePassThrough__exclusive")]
+        streams = [Stream(lines=lines, detectors=[])]
+        return Streams(streams=streams)
+
+    default_VeloCluster_source.global_bind(bank_type="VPRetinaCluster")
+    make_VeloClusterTrackingSIMD.global_bind(
+        algorithm=VeloRetinaClusterTrackingSIMD)
+
+    public_tools = [
+        trackMasterExtrapolator_with_simplified_geom(),
+        stateProvider_with_simplified_geom()
+    ]
+    full_lines_for_TISTOS = [
+        linename for line_dict in [qee_single_muon_lines, qee_dimuon_lines]
+        for linename in line_dict.keys()
+    ]
+    with list_of_full_stream_lines.bind(lines=full_lines_for_TISTOS):
+        return run_moore(options, _make_streams, public_tools=public_tools)
+
+
+def Turbo(options: Options):
+    def _make_streams():
+        streams = [
+            Stream(lines=[PassLine(name="PassLine")], detectors=DETECTORS)
+        ]
+        return Streams(streams=streams)
+
+    return run_moore(options, _make_streams, public_tools=[])
+
+
+def TurCal(options: Options):
+    def make_turcal_spruceline(hlt2_linename, persistreco=False, prescale=1.):
+        line_filter = f"{hlt2_linename}Decision"
+        location = f"/Event/HLT2/{hlt2_linename}/Particles"
+        spruce_linename = hlt2_linename.replace("Hlt2", "SpruceTurCal")
+        print(f"{line_filter} {location} {spruce_linename}")
+        hlt2_particles = get_particles(location)
+        turcal_spruce_line = SpruceLine(
+            name=spruce_linename,
+            hlt2_filter_code=line_filter,
+            algs=[hlt2_particles],
+            persistreco=persistreco,
+            prescale=prescale)
+        return turcal_spruce_line
+
+    def _make_streams():
+        lines = [
+            make_turcal_spruceline(linename, persistreco=True)
+            for linename in z_trackeff_lines.keys()
+        ]
+        lines += [pass_through_line(name="SprucePassThrough__turcal")]
+        streams = [Stream(lines=lines, detectors=DETECTORS)]
+        return Streams(streams=streams)
+
+    with list_of_full_stream_lines.bind(lines=[]):
+        return run_moore(options, _make_streams, public_tools=[])