agentsclimarketplace

Protflow

Skill LiorZ/protein-design-skills/skills/protflow

Build composable protein design pipelines with ProtFlow — a Python framework that wraps RFdiffusion, RFdiffusion3, LigandMPNN/ProteinMPNN/SolubleMPNN, ESMFold, ColabFold (AF2), AlphaFold3, Boltz, Caliby, Frame2Seq, ProteinGenerator, PLACER, AttnPacker, SigmaDock, Rosetta, GROMACS, DSSP, FPocket, Propka, TM-align, RMSD/biopython metrics, and more behind a single Poses + JobStarter + Runner API, with first-class SLURM array-job and local execution. Use this skill when: (1) Composing multi-step design pipelines (generate backbone → design sequence → predict structure → score → filter → iterate) without writing bespoke cluster-management code. (2) Running any of the supported tools on a SLURM cluster as array jobs, or locally as parallel subprocesses, behind a unified interface. (3) Managing a pandas-DataFrame-backed `Poses` object that carries all structures, sequences, scores, residue selections, and motifs through the pipeline, persisted as JSON/CSV/Pickle/Parquet/Feather. (4) Filtering / ranking / composite-scoring designs with `filter_poses_by_rank`, `filter_poses_by_value`, and `calculate_composite_score`. (5) Tracking residue motifs across generative steps (e.g. RFdiffusion contig → designed → predicted) using `ResidueSelection` and motif remapping. (6) Adding a new tool / runner to a pipeline by subclassing `Runner` and following the standard `run()` → DataFrame contract. (7) Wiring tool paths and Python environments via `~/.config/protflow/config.py` (the `protflow-init-config` / `protflow-set-config` / `protflow-check-config` CLI tools). (8) Debugging a runner crash by reading the captured stderr, the runner's score file, or the SLURM accounting via `get_SLURM_stats`. Covers the Poses/JobStarter/Runner architecture, the `prefix` convention, options vs `pose_options` vs `pose_opt_cols`, residue selections, the config.py system, the runner/metric catalog, SLURM vs local job starters, motif tracking through RFdiffusion, common pipeline patterns (de novo monomer, binder design with validation, ligand co-folding, enzyme design, redesign + relaxation), troubleshooting, and how to extend ProtFlow with a custom runner. Pairs with: `rfdiffusion`, `proteinmpnn`, `ligandmpnn`, `solublempnn`, `bindcraft`, `boltzgen`, `boltz`, `alphafold`, `chai`, `esm`, `protein-qc`, `ipsae`, `binder-design`, `protein-design-workflow`, `campaign-manager`.From its SKILL.md

Install
npx -y skills add LiorZ/protein-design-skills --skill protflow

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its file declares

Copied from the file, not written here

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

34.0 KB, as published. Nobody here has run it

ProtFlow — Protein Design Pipelines on SLURM (and Locally)

What this is

ProtFlow is a Python package that runs protein design tools on SLURM clusters behind a single, uniform API. You write a pipeline as a sequence of runner.run(poses, prefix=...) calls; ProtFlow handles submission, parallelisation, output collection, score-DataFrame merging, and intermediate storage.

The framework is built around three primary abstractions:

AbstractionWhat it isLives in
PosesA pandas DataFrame of structures/sequences/scores plus a work_dir.protflow.poses
JobStarterA submission backend: SLURM array (SbatchArrayJobstarter) or local.protflow.jobstarters
RunnerA wrapper around one external tool (RFdiffusion, ESMFold, ...).protflow.tools.*, protflow.metrics.*

Everything else — residue selections, motif tracking, composite scoring, filtering, plotting — is built on top.

Key contract: every runner.run(poses, prefix="foo", ...) call mutates and returns the same Poses object (it both updates poses.df in place and returns it for chaining). Output files land under poses.work_dir/foo/ and a score file foo_scores.<storage_format> is created there. Every new column added to poses.df is prefixed with foo_ to prevent name collisions across runs.

When to reach for this skill

Reach for ProtFlow when the user wants to:

  • Chain multiple design tools (e.g. RFdiffusion → LigandMPNN → ESMFold → RMSD).
  • Run any of the supported tools across a SLURM cluster without writing sbatch scripts or polling logic.
  • Iterate on designs while keeping all metadata in one DataFrame they can filter, plot, and export.
  • Add a new external tool to an existing pipeline without bespoke glue code.

Do not reach for this skill when the user wants to:

  • Run a single one-shot inference of one tool (just call that tool directly, or use that tool's own skill — rfdiffusion, bindcraft, boltz, etc.).
  • Replace a SLURM-aware Snakemake/Nextflow pipeline (ProtFlow is Python-only, in-process orchestration; no DAG engine, no resume-from-checkpoint other than score-file caching).

Prerequisites

RequirementRecommendedNotes
OSLinuxTested on cluster login + compute nodes. macOS works for LocalJobStarter only.
Python≥ 3.11Hard requirement; pyproject.toml enforces this.
SLURMRecent SLURM with sbatch/squeue/sacctRequired only for SbatchArrayJobstarter. LocalJobStarter works anywhere.
EnvironmentsOne conda/mamba env per toolRFdiffusion, ESMFold, LigandMPNN, AF2/AF3, Boltz, etc. each want their own — config.py points at the right Python binary per tool.
DiskA shared scratch / project directoryEvery run writes a work_dir/<prefix>/ subtree. Plan for tens to hundreds of GB on a real campaign.

Installation (three steps)

# 1) Clone and install (editable; package is under active development)
git clone https://github.com/mabr3112/ProtFlow.git
cd ProtFlow
conda create -n protflow python=3.11 -y
conda activate protflow
pip install -e .

# 2) Initialise config.py (writes ~/.config/protflow/config.py)
protflow-init-config

# 3) (Optional) point at a custom config.py — useful on shared clusters:
protflow-set-config /shared/protflow/config.py
protflow-check-config        # confirms which file is in use
protflow-set-config --unset  # revert to the default search path

The config search order is (0) the pointer file saved by protflow-set-config, (1) $PROTFLOW_CONFIG, (2) $XDG_CONFIG_HOME/protflow/config.py (default ~/.config/protflow/config.py), (3) the bundled protflow/config.py. See references/config.md for the full schema — which env-var-style constants each runner expects to find, what they should point to, and what *_PRE_CMD is for.

The thirty-second example

from protflow.poses import Poses
from protflow.jobstarters import SbatchArrayJobstarter
from protflow.tools.rfdiffusion import RFdiffusion
from protflow.tools.ligandmpnn import LigandMPNN
from protflow.tools.esmfold import ESMFold
from protflow.metrics.rmsd import BackboneRMSD

gpu = SbatchArrayJobstarter(max_cores=10, gpus=1)

poses = Poses(poses=None, work_dir="./demo_run", jobstarter=gpu, storage_format="pickle")

# 1) De novo 70mers
poses = RFdiffusion().run(poses, prefix="diff", num_diffusions=5,
                          options="'contigmap.contigs=[70-70]'")

# 2) 8 sequences per backbone with SolubleMPNN
poses = LigandMPNN().run(poses, prefix="mpnn", nseq=8, model_type="soluble_mpnn")

# 3) Predict structures with ESMFold
poses = ESMFold().run(poses, prefix="esm")

# 4) Backbone RMSD vs the original RFdiffusion structure
poses = BackboneRMSD(ref_col="diff_location").run(poses, prefix="rmsd")

# 5) Filter to ≤2 Å self-consistent designs, top-50 by predicted pLDDT
poses.filter_poses_by_value(score_col="rmsd_rmsd", value=2.0, operator="<=")
poses.filter_poses_by_rank(score_col="esm_plddt", n=50, ascending=False)
poses.save_scores()
poses.save_poses(out_path="./demo_run/final_designs/")

Everything in this snippet is already standard ProtFlow: no SLURM template, no custom argparse, no manual score parsing. The Poses object grows columns diff_*, mpnn_*, esm_*, rmsd_* as it goes, and the on-disk layout is predictable (./demo_run/diff/, ./demo_run/mpnn/, ./demo_run/esm/, …).

The Poses object — what it does

Poses is the single source of truth for a campaign. It is a thin wrapper around poses.df: pd.DataFrame plus directory management.

You want to ...Use ...
Load PDBs from a directoryPoses(poses="some/dir/", glob_suffix="*.pdb")
Load from a saved score filePoses(poses="prev_run_scores.json") or load_poses(path)
Set the work directory after constructionposes.set_work_dir("./run/")
Filter to top-N by a scoreposes.filter_poses_by_rank(n=50, score_col=..., ascending=False)
Filter by valueposes.filter_poses_by_value(score_col=..., value=2.0, operator="<=")
Build a composite ranking scoreposes.calculate_composite_score(name=..., scoreterms=[...], weights=[...])
Aggregate scores across pose groupsposes.calculate_mean_score(...) / _median_ / _max_ / _min_ / _std_
Persist to disk between stepsposes.save_scores() (auto-named in work_dir)
Copy final PDBs outposes.save_poses("./final_pdbs/")
Replicate each pose N× (e.g. for sequence design)poses.duplicate_poses("./multiplexed/", n_duplicates=N)
Reset poses to the original inputsposes.reset_poses()
Build a Bio.PDB.Structure for inspectionposes.get_pose("pose_description")
Rebuild the index after mergesposes.reindex_poses(prefix=..., remove_layers=1)

poses.df always carries three mandatory columns:

  • input_poses — original input path (never overwritten)
  • poses — current "active" pose path; this is what runners read from
  • poses_description — basename without extension; used to merge scores back

Runner outputs append {prefix}_location, {prefix}_description, and score columns named {prefix}_<scoreterm>.

See references/poses.md for the full method list with semantics.

JobStarters — local vs SLURM

JobStarter decides how a runner's commands are executed.

from protflow.jobstarters import LocalJobStarter, SbatchArrayJobstarter

local      = LocalJobStarter(max_cores=4)
slurm_cpu  = SbatchArrayJobstarter(max_cores=50, gpus=False)
slurm_gpu  = SbatchArrayJobstarter(max_cores=10, gpus=1, options="--time=08:00:00 --partition=gpu")

Resolution priority when a runner runs is

runner.run(jobstarter=...)  >  Runner(jobstarter=...)  >  Poses(jobstarter=...)

so a per-run override always wins, then the runner's default, then the Poses default. This is exactly what you want when most of the pipeline is one backend but a single step needs different resources (e.g. CPU LigandMPNN inside an otherwise GPU pipeline).

Notes worth knowing:

  • SbatchArrayJobstarter chunks commands into SLURM job arrays, capping concurrent tasks at max_cores. Arrays longer than 1000 are split.
  • batch_cmds=N (constructor or .start() arg) merges short commands into N composite array tasks via ;-joining. Use this when you have thousands of fast commands.
  • options="..." is appended verbatim to every sbatch. Use for --time, --partition, --qos, --mem, etc.
  • gpus=1 adds --gpus-per-node 1. Set gpus=False for CPU partitions.
  • wait=True (the default) blocks until the array drains. ProtFlow uses this so the next step in your script can rely on outputs being on disk.
  • get_SLURM_stats(job_name, start_time=...) queries sacct for elapsed / CPU / state aggregates — useful for cost tracking. Must be called on a login node where sacct is available.

See references/jobstarters.md for the full table and references/slurm.md for cluster-side caveats (array limits, sacct quirks, error log layout).

The Runner contract

Every Runner exposes the same call shape:

runner = SomeRunner(jobstarter=..., **constructor_kwargs)   # optional config overrides
poses  = runner.run(
    poses,
    prefix="step_name",            # mandatory; namespaces output columns & dir
    jobstarter=...,                # optional per-call override
    options=...,                   # global options (string in tool's native format)
    pose_options=...,              # per-row options: a list or a poses.df column name
    overwrite=False,               # if True, blow away previous results
    **tool_specific_kwargs,
)

What happens inside, every time:

  1. The runner validates that prefix isn't already a column in poses.df (so you can't accidentally clobber a previous step).
  2. It picks a jobstarter using the priority chain above.
  3. It creates poses.work_dir/<prefix>/ and writes intermediate files there.
  4. It checks for an existing <prefix>_scores.<storage_format> and — if present and overwrite=False — short-circuits, merging cached scores back into poses.df without re-running the tool. This is how you can re-run the same pipeline script after fixing a downstream step.
  5. It builds one command per pose (or per batch), launches them through the jobstarter, waits for completion.
  6. It collects results into a DataFrame with description + location + score columns, persists it to the score file, and merges into poses.df via RunnerOutput.

The base class auto-wraps every subclass's run() so that if score collection crashes, the exception is re-raised as Runner.CrashError with the tail of the SLURM/local stderr appended — this is the single most useful debugging affordance ProtFlow provides. Always read the appended log block when a runner fails.

references/runners-pattern.md walks through the contract in detail and shows how to subclass Runner for a new tool.

options vs pose_options vs pose_opt_cols

Three flavours, all valid simultaneously:

FormScopeHow to pass itExample
optionsEvery poseA string in the tool's native CLI syntaxoptions="--temperature 0.05 --seed 1"
pose_optionsPer-poseA list (same length as poses) OR the name of a poses.df column whose cells are stringsposes.df["my_opts"] = [...]; runner.run(..., pose_options="my_opts")
pose_opt_colsPer-pose, structuredA dict mapping the tool's option name → poses.df column whose cells are values (often ResidueSelections, lists, paths)pose_opt_cols={"fixed_residues": "active_site"} (LigandMPNN)

Pose-specific values override global values where they conflict. Use options for everything that's constant across the campaign, and reserve pose_options / pose_opt_cols for things that legitimately vary per design (input motifs, contigs, hotspots, fixed residues, …).

references/options-and-pose-options.md lists the exact syntax accepted by each runner.

Residue selections and motif tracking

protflow.residues.ResidueSelection is the canonical way to talk about "which residues" inside a pose. It survives serialization through JSON / CSV / pickle and can be re-mapped after a generative step.

from protflow.residues import ResidueSelection, from_contig, from_dict

motif = ResidueSelection("A12,A34,A56-A60")     # string form
motif = from_contig("A12-A20, B5")              # contig form
motif = from_dict({"A": [12, 13, 14], "B": [5]}) # dict form

motif.to_string()          # 'A12,A13,A14,B5'
motif.to_list()            # ['A12','A13','A14','B5']
motif.to_dict()            # {'A':[12,13,14], 'B':[5]}
motif.to_rfdiffusion_contig()  # for handing to RFdiffusion as a contigmap

Store a ResidueSelection in a poses.df column to carry it through the pipeline. After RFdiffusion (where residue indices change), call rfdiff.run(..., update_motifs=["my_motif_col"]) and the column is remapped to the new indices automatically using RFdiffusion's con_ref_pdb_idx/con_hal_pdb_idx outputs.

For computing residue selections from a pose, ProtFlow provides four selectors in protflow.tools.residue_selectors:

  • ChainSelector(chain=...) / ChainSelector(chains=[...])
  • TrueSelector() — every residue
  • NotSelector(residue_selection=..., contig=...) — complement
  • DistanceSelector(center=..., distance=..., operator=..., center_atoms=..., noncenter_atoms=..., include_center=...) — atom-distance-based, useful for pocket / interface definitions

Each selector's .select(prefix=...) writes a new column {prefix}_residue_selection of ResidueSelection objects to poses.df.

There is also AtomSelection (in protflow.residues) for atom-level operations including from_rfd3_input_spec to parse Foundry/RFD3 specifications.

See references/residue-selection.md for the full reference.

Runner catalog

The catalog below lists every Runner shipped with ProtFlow as of master. Each row has: import path, what it does, GPU/CPU, and the config.py path variable it expects. Constructor signatures all accept (*, jobstarter=None) plus tool-specific overrides; .run() always takes poses, prefix, ... and returns the updated Poses.

Backbone generation

RunnerToolGPU?Config keys
protflow.tools.rfdiffusion.RFdiffusionRFdiffusionyesRFDIFFUSION_SCRIPT_PATH, RFDIFFUSION_PYTHON_PATH, RFDIFFUSION_PRE_CMD
protflow.tools.rfdiffusion3.RFdiffusion3RFdiffusion3 / Foundry RFD3yesRFDIFFUSION3_BIN_PATH, RFDIFFUSION3_PYTHON_PATH, RFDIFFUSION3_MODEL_DIR, RFDIFFUSION3_PRE_CMD
protflow.tools.protein_generator.ProteinGeneratorprotein-generatoryesPROTEIN_GENERATOR_SCRIPT_PATH, PROTEIN_GENERATOR_PYTHON_PATH

Sequence design (inverse folding)

RunnerToolGPU?Config keys
protflow.tools.ligandmpnn.LigandMPNNLigandMPNN / ProteinMPNN / SolubleMPNN / Membrane MPNN — model_type selectsyesLIGANDMPNN_SCRIPT_PATH, LIGANDMPNN_PYTHON_PATH, LIGANDMPNN_PRE_CMD
protflow.tools.frame2seqdesign.Frame2SeqDesignFrame2Seq designyesFRAME2SEQ_PYTHON_PATH, FRAME2SEQ_PRE_CMD
protflow.tools.caliby.CalibySequenceDesignCaliby sequence designyes(Caliby paths)
protflow.tools.caliby.CalibyEnsembleSeqDesignCaliby ensemble-aware designyes(Caliby paths)

Structure prediction / validation

RunnerToolGPU?Config keys
protflow.tools.esmfold.ESMFoldESMFold (single-chain)yesESMFOLD_PYTHON_PATH, ESMFOLD_PRE_CMD
protflow.tools.esm.ESMESM2 embeddings / PLLsyesESM_PYTHON_PATH, ESM_PRE_CMD
protflow.tools.colabfold.ColabfoldLocalColabFold (AF2)yesCOLABFOLD_SCRIPT_PATH, COLABFOLD_PRE_CMD
protflow.tools.alphafold3.AlphaFold3AlphaFold 3yesALPHAFOLD3_SCRIPT_PATH, ALPHAFOLD3_PYTHON_PATH, ALPHAFOLD3_PRE_CMD
protflow.tools.boltz.BoltzBoltz-1 / Boltz-2yesBOLTZ_PATH, BOLTZ_PYTHON, BOLTZ_PRE_CMD
protflow.tools.minifold.MinifoldMinifoldyesMINIFOLD_SCRIPT_PATH, MINIFOLD_PYTHON_PATH, MINIFOLD_PRE_CMD
protflow.tools.caliby.CalibyEnsembleGeneratorCaliby ensemble structuresyes(Caliby paths)

Refinement / repacking / docking

RunnerToolGPU?Config keys
protflow.tools.rosetta.RosettaRosetta (any application: relax, scripts, fixbb, ...)cpuROSETTA_BIN_PATH, ROSETTA_PRE_CMD
protflow.tools.attnpacker.AttnPackerAttnPacker (sidechain packing)yesATTNPACKER_PYTHON_PATH, ATTNPACKER_DIR_PATH, ATTNPACKER_PRE_CMD
protflow.tools.placer.PLACERPLACER (sidechain repacking)yesPLACER_SCRIPT_PATH, PLACER_PYTHON_PATH, PLACER_PRE_CMD
protflow.tools.gnina.GNINAGNINA dockingyes(GNINA paths)
protflow.tools.sigmadock.SigmaDockSigmaDockyesSIGMADOCK_SCRIPT_PATH, SIGMADOCK_PYTHON_PATH, SIGMADOCK_CKPT_PATH, SIGMADOCK_PRE_CMD
protflow.tools.gromacs.GromacsGROMACS MDcpu (or GPU)GROMACS_PATH
protflow.tools.gromacs.MDAnalysisMDAnalysis post-processingcpu(auxiliary script)

Edits / utilities (protflow.tools.protein_edits)

RunnerWhat it does
ChainAdderAdd a chain (from another pose / sequence)
ChainRemoverRemove chains by ID, or keep only listed chains
SequenceAdderInsert a sequence into one or more chains
SequenceRemoverStrip residue ranges from one or more chains

Metrics (protflow.metrics.*)

These are Runners, not just functions — they obey the same prefix / work_dir discipline, and emit columns like every other runner.

RunnerWhat it computesConfig keys
protflow.metrics.rmsd.BackboneRMSDCA / backbone RMSD vs a reference column—
protflow.metrics.rmsd.AtomRMSDRMSD over arbitrary atom selections—
protflow.metrics.rmsd.MotifRMSDRMSD between two motif selections—
protflow.metrics.tmscore.TMalign / TMscoreTM-score / TM-alignTMALIGN_PATH*
protflow.metrics.dssp.DSSPDSSP secondary-structure assignmentDSSP_PATH
protflow.metrics.fpocket.FPocketPocket detection / volume / druggabilityFPOCKET_PATH
protflow.metrics.propka.PropkapKa / electrostaticsPROPKA_PATH*
protflow.metrics.protparam.ProtParamBiopython ProtParam (pI, instability, GRAVY, MW, εₐ)—
protflow.metrics.frame2seqscore.Frame2SeqScoreFrame2Seq sequence-given-structure scoreFRAME2SEQ_PYTHON_PATH
protflow.metrics.selection_identity.SelectionIdentityOne-letter / three-letter identity of a selection—
protflow.metrics.ligand.LigandClashesHeavy-atom clashes between a chain and the rest—
protflow.metrics.ligand.LigandContactsHeavy-atom contacts (with optional element filters)—
protflow.metrics.biopython_metrics.BiopythonMetricRunnerRun a list of biopython metrics (Distance, Angle, …)—
protflow.metrics.generic_metric_runner.GenericMetricRun any Python module/function as a metric—

* Some paths fall back to PATH lookup if not set; see the per-runner docstring.

references/runners-catalog.md documents every runner's run() signature, expected score columns, and the typical options strings.

Pipeline patterns

The five canonical pipelines below are written out fully under examples/. Pick the closest match and adapt.

PatternExample script
Minimal "load pdbs → score → filter"examples/minimal_pipeline.py
De novo monomer: RFdiffusion → MPNN → ESMFold → BB RMSDexamples/rfdiffusion_mpnn_esm.py
Binder design with validation: RFdiff (hotspots) → LigandMPNN → AF2/AF3 → ipTM filterexamples/binder_design_rfdiff_validate.py
Ligand co-folding: load fastas → AlphaFold3 with SMILESexamples/predict_protein_ligand.py
Enzyme redesign: load pocket → LigandMPNN (fixed catalytic) → Rosetta relax → metricsexamples/enzyme_redesign.py

The general recipe in every case is:

poses = Poses(...).set_jobstarter(...)              # 1. Load inputs
poses = backbone_gen.run(poses, prefix="bb", ...)    # 2. Generate / load backbones
poses = seq_design.run(poses, prefix="seq", ...)     # 3. Sequence design
poses = predictor.run(poses, prefix="pred", ...)     # 4. Structure prediction / validation
poses = metric.run(poses, prefix="m", ...)           # 5. Quantitative scoring
poses.filter_poses_by_value(...)                     # 6. Filter
poses.filter_poses_by_rank(...)                      # 6.
poses.calculate_composite_score(...)                 # 7. (Optional) composite ranking
poses.save_scores(); poses.save_poses("out/")       # 8. Persist

When you re-run the script after a crash, ProtFlow short-circuits any step whose <prefix>_scores.<storage_format> file already exists — so debugging is "fix the broken step, re-run the script, watch only the broken step re-execute".

Troubleshooting

The single most common failure mode is a runner failing inside collect_scores(). ProtFlow's auto-wrapper appends the tail of the SLURM / local stderr to the exception. Read the appended log first — it is almost always the real error (CUDA OOM, missing input file, malformed contig, etc.), not the Python traceback above it.

Other failure modes and fixes:

SymptomLikely causeFix
MissingConfigError at import / first runner buildNo config.py resolvableprotflow-init-config (or set $PROTFLOW_CONFIG).
ProtFlowConfigError: Missing parameter ... in config.pyA required key isn't in config.pyCopy the matching line from config_template.py into your config.py and fill it in.
MissingConfigSettingError: Variable ... not specifiedKey exists but the value is ""Set the value (path to script / python / pre_cmd).
FileNotFoundError: Path set for X does not existThe path in config.py points somewhere invalidFix the path. Use protflow-check-config to identify which file you're editing.
KeyError: Column prefix already taken in poses.dfYou re-used a prefix across two .run() callsPick a unique prefix. ProtFlow refuses to overwrite.
Runner returns "Found existing scorefile ... returning N poses without running" but you expected a fresh runCached score file from a previous runPass overwrite=True (or delete <work_dir>/<prefix>/<prefix>_scores.*).
SLURM jobs run forever / never appear in squeueBad options (e.g. invalid --partition)Check <work_dir>/<prefix>/<jobname>_slurm.err and _jobstarter.log.
RuntimeError: Number of output poses is smaller than ...Some array tasks crashedInspect the appended stderr block in the exception; rerun with overwrite=False after fixing the crashed inputs.
KeyError: Could not find <col> in poses dataframe!Referenced a column that wasn't createdRun the step that produces that column, or fix the column name.
ProtFlow ignores a pose_options columnPassed a literal list with wrong length, or wrong column nameLength must equal len(poses); spelling must match poses.df exactly.

references/troubleshooting.md has the long-form walkthrough.

Extending ProtFlow with a custom runner

Every runner is a subclass of protflow.runners.Runner with three methods:

from protflow.runners import Runner, RunnerOutput
from protflow.poses import Poses
from protflow.jobstarters import JobStarter

class MyTool(Runner):
    def __init__(self, jobstarter: JobStarter = None):
        self.jobstarter = jobstarter
        self.index_layers = 1   # how many '_N' suffixes the tool appends per pose

    def __str__(self):
        return "mytool"

    def run(self, poses: Poses, prefix: str, jobstarter: JobStarter = None,
            options: str = None, overwrite: bool = False) -> Poses:
        # 1) Standard setup
        work_dir, jobstarter = self.generic_run_setup(
            poses=poses, prefix=prefix,
            jobstarters=[jobstarter, self.jobstarter, poses.default_jobstarter]
        )

        # 2) Cache-hit short-circuit
        scorefile = f"{work_dir}/{prefix}_scores.{poses.storage_format}"
        if (scores := self.check_for_existing_scorefile(scorefile, overwrite)) is not None:
            return RunnerOutput(poses=poses, results=scores, prefix=prefix,
                                index_layers=self.index_layers).return_poses()

        # 3) Build cmds (one per pose, or batched)
        cmds = [f"mytool --in {p} --out {work_dir}/{d}.out"
                for p, d in zip(poses.poses_list(), poses.df['poses_description'])]

        # 4) Submit & wait
        jobstarter.start(cmds=cmds, jobname="mytool", wait=True, output_path=work_dir)

        # 5) Collect into a DataFrame with mandatory 'description' + 'location'
        scores = my_collect_function(work_dir)
        self.save_runner_scorefile(scores=scores, scorefile=scorefile)

        # 6) Merge back into poses
        return RunnerOutput(poses=poses, results=scores, prefix=prefix,
                            index_layers=self.index_layers).return_poses()

That's the entire contract. The base class's __init_subclass__ will auto-wrap your run() to attach stderr context on crash.

references/runners-pattern.md shows three real subclasses (ESMFold end-to-end, LigandMPNN with pose_opt_cols, BackboneRMSD as a non-jobstarter local Runner) annotated line-by-line.

Where to look next

  • references/installation.md — full installation matrix and pin notes.
  • references/config.md — every *_PATH / *_PYTHON_PATH / *_PRE_CMD key.
  • references/poses.md — every Poses method with semantics and gotchas.
  • references/jobstarters.md — local vs SLURM, batching, error capture, sacct.
  • references/slurm.md — cluster-side tips (squeue -n, array limits, --open-mode).
  • references/runners-pattern.md — the Runner contract, annotated examples.
  • references/runners-catalog.md — every runner's run() signature.
  • references/options-and-pose-options.md — options / pose_options / pose_opt_cols syntax per runner.
  • references/residue-selection.md — ResidueSelection, AtomSelection, the four selectors.
  • references/motif-tracking.md — how update_motifs works through RFdiffusion.
  • references/filtering-and-scoring.md — filter_poses_by_* and calculate_*_score.
  • references/metrics.md — every metric runner with output columns.
  • references/troubleshooting.md — failure modes and fixes.
  • references/cli-tools.md — protflow-init-config, protflow-set-config, protflow-check-config.
  • examples/ — full runnable scripts.

Keep looking

Skills are one crate of 628,971. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.