Skip to content

Getting Started

This guide walks through one complete Culsma run: write a representative flow-cytometry protocol, run it, read the terminal result, save execution artifacts, and replay the run.

Before starting, install Culsma and confirm that culsma --help works. See Install Culsma for the short install path.

What You Will Do

  • create a protocol source file
  • define containers with canonical content kind, type, and attrs
  • model staining, washing, fixation, and acquisition preparation
  • use repeat, sep, with env, markers, stream, and readout schema syntax
  • run the protocol through the kernel pipeline
  • inspect the returned acquisition sample and saved artifacts
  • replay the runtime event stream

Create a Protocol File

Create a file named flow_cytometry_protocol.culs:

text
protocol FlowCytometryProtocol {
    // A compact, generic flow-cytometry immunophenotyping protocol.
    // The example demonstrates material setup, washing, staining, fixation,
    // single-cell stream construction, and a structured readout declaration.

    // Prepare the cell input and process reagents.
    let cell_suspension = tube(
        label = "PreparedCellSuspension",
        capacity = 2000uL,
        load = [content(kind = bio_cellular, type = cell_population, code = "CELLS1", attrs = { state: suspension }):500uL]
    );
    let lysis_buffer = tube(
        label = "RedCellLysisBuffer",
        capacity = 3000uL,
        load = [content(kind = formulation, type = buffer, code = "RCLB", attrs = { role: lysis }):2500uL]
    );
    let staining_buffer = tube(
        label = "StainingBuffer",
        capacity = 5000uL,
        load = [content(kind = formulation, type = buffer, code = "STAINBUF", attrs = { role: wash }):4500uL]
    );
    let acquisition_buffer = tube(
        label = "AcquisitionBuffer",
        capacity = 2000uL,
        load = [content(kind = formulation, type = buffer, code = "ACQBUF", attrs = { role: resuspension }):1500uL]
    );
    let viability_dye = tube(
        label = "ViabilityDye",
        capacity = 200uL,
        load = [content(kind = chemical, type = dye, code = "LIVEDEAD", attrs = { role: stain }):100uL]
    );
    let antibody_cocktail = tube(
        label = "FluorAntibodyCocktail",
        capacity = 500uL,
        load = [content(kind = bio_molecule_or_virus, type = protein, code = "AB_CD45_CD3_CD19", attrs = { role: antibody }):300uL]
    );
    let fixation_buffer = tube(
        label = "FixationBuffer",
        capacity = 1000uL,
        load = [content(kind = formulation, type = buffer, code = "FIX", attrs = { role: fixation }):800uL]
    );
    let waste = tube(label = "FlowWaste", capacity = 10000uL);
    let working_cells = tube(label = "WorkingCellSuspension", capacity = 5000uL);
    let acquisition_sample = tube(label = "AcquisitionSample", capacity = 3000uL);

    // Combine cells with lysis buffer and incubate at room temperature.
    working_cells << [cell_suspension:200uL, lysis_buffer:1800uL] with constraint(high_precision, low_carryover);
    with env(thermal = 25C, duration = 10min) {
        hold(working_cells);
    }

    // Retain the cell-containing fraction and route process liquid to waste.
    let lysis_split = sep(sample = working_cells, program = filtration_program(membrane = "cell_retention", drive = "centrifuge"));
    waste << [lysis_split[0]];
    working_cells << [lysis_split[1]];

    // Wash retained cells twice before staining.
    repeat wash_cycle in schedule(start = 1, end = 2, step = 1) {
        working_cells << [staining_buffer:1000uL];
        let wash_split = sep(sample = working_cells, program = filtration_program(membrane = "cell_retention", drive = "centrifuge"));
        waste << [wash_split[0]];
        working_cells << [wash_split[1]];
    }

    // Add viability dye and antibody cocktail under dark-protected conditions.
    working_cells << [viability_dye:5uL, antibody_cocktail:50uL] with constraint(high_precision, low_carryover, dark_protected);
    with constraint(dark_protected) {
        with env(thermal = 4C, duration = 30min) {
            hold(working_cells);
        }
    }

    // Fix stained cells, then prepare the acquisition sample.
    working_cells << [fixation_buffer:300uL];
    with constraint(dark_protected) {
        with env(thermal = 25C, duration = 10min) {
            hold(working_cells);
        }
    }

    acquisition_sample << [working_cells:300uL, acquisition_buffer:700uL];

    // Declare the marker panel, single-cell event stream, and readout schema.
    let immunophenotyping_panel = markers([CD45, CD3, CD19, LiveDead]);
    let cell_events = stream(sample = acquisition_sample, unit = single_cell, panel = immunophenotyping_panel);
    let flow_event_schema = data_schema(
        label = "FlowCytometryEventReadout",
        fields = [event_id, sample_id, fsc_a, ssc_a, cd45_signal, cd3_signal, cd19_signal, viability_signal, population_label]
    );
    let flow_events = img(sample = cell_events, quantity = customized, schema_ref = flow_event_schema, save_raw = true);

    return acquisition_sample;
}

This example models a compact, generic flow-cytometry immunophenotyping protocol. It keeps the protocol small enough for a first run while exercising the same source surface used by larger protocols: canonical content taxonomy, metadata attrs, material transfer, separation, environment blocks, repeat scheduling, marker panels, stream construction, and readout declaration.

Run It

From the directory containing flow_cytometry_protocol.culs, run:

bash
culsma flow_cytometry_protocol.culs

The shorthand above is equivalent to:

bash
culsma run flow_cytometry_protocol.culs

Expected terminal output:

text
FlowCytometryProtocol ok

return:
  AcquisitionSample (tube)
    volume: 1000 uL
    mass: 1000 mg

execution: 46/46 steps completed, 0 diagnostics

Read the Result

The result has four useful signals:

OutputMeaning
FlowCytometryProtocol okThe source parsed, validated, planned, and ran successfully.
AcquisitionSample (tube)The protocol returned the prepared acquisition sample container.
volume: 1000 uLThe runtime tracked the final material amount in that container.
46/46 steps completed, 0 diagnosticsEvery generated execution step completed without diagnostics.

The returned tube is the protocol output. It is separate from the generated run report and debug artifacts.

Read the Source

The protocol is a compact flow-cytometry protocol. These are the main source sections to inspect.

Content Initialization

text
load = [content(kind = bio_cellular, type = cell_population, code = "CELLS1", attrs = { state: suspension }):500uL]

kind and type identify the broad material class and refinement. code is a stable component identifier. attrs records additional metadata such as preparation state or protocol role.

Material Preparation

text
working_cells << [cell_suspension:200uL, lysis_buffer:1800uL] with constraint(high_precision, low_carryover);
with env(thermal = 25C, duration = 10min) {
    hold(working_cells);
}

The << operator transfers material into a destination container. The with constraint(...) clause records execution requirements, and with env(...) applies temperature and duration context to the enclosed hold.

Separation and Washing

text
let lysis_split = sep(sample = working_cells, program = filtration_program(membrane = "cell_retention", drive = "centrifuge"));
waste << [lysis_split[0]];
working_cells << [lysis_split[1]];

sep(...) produces indexed fraction outputs. In this example, slot 0 is routed to waste and slot 1 is retained as the working cell material. The repeat block applies the same wash pattern twice.

Staining and Fixation

text
working_cells << [viability_dye:5uL, antibody_cocktail:50uL] with constraint(high_precision, low_carryover, dark_protected);

The viability dye is represented as chemical/dye with attrs = { role: stain }. The antibody cocktail is represented as bio_molecule_or_virus/protein with attrs = { role: antibody }. These roles are metadata for protocol intent; they do not imply biological prediction.

Stream and Readout

text
let immunophenotyping_panel = markers([CD45, CD3, CD19, LiveDead]);
let cell_events = stream(sample = acquisition_sample, unit = single_cell, panel = immunophenotyping_panel);

The marker panel and stream declaration model the acquisition surface. The final img(...) call declares a structured readout schema for event-level signals.

Save Execution Artifacts

Run again with an artifacts directory:

bash
culsma run flow_cytometry_protocol.culs --artifacts-dir tmp/flow-cytometry-run

The directory contains staged pipeline and runtime artifacts:

FileMeaning
ast.jsonParsed source program.
ir.jsonCanonical IR after compile.
validate.jsonSemantic validation diagnostics.
typecheck.jsonType and unit diagnostics.
plan.jsonExecutable plan.
run.jsonRuntime state, events, diagnostics, and user result.
output.jsonMachine-readable protocol return plus report.
result.jsonDerived lab_report_v1 report summary.

Replay It

Replay reconstructs runtime state from the saved event stream:

bash
culsma replay --run-json tmp/flow-cytometry-run/run.json --out tmp/flow-cytometry-run/replayed_state.json

For this example, replay reconstructs 46 executed steps from 94 runtime events.

Next Steps

Released under the Apache-2.0 license.