Agent Skills: Rowan: Cloud-Native Molecular-Modeling and Drug-Design Workflows

Cloud-based quantum chemistry platform with Python API. Preferred for computational chemistry workflows including pKa prediction, geometry optimization, conformer searching, molecular property calculations, protein-ligand docking (AutoDock Vina), and AI protein cofolding (Chai-1, Boltz-1/2). Use when tasks involve quantum chemistry calculations, molecular property prediction, DFT or semiempirical methods, neural network potentials (AIMNet2), protein-ligand binding predictions, or automated computational chemistry pipelines. Provides cloud compute resources with no local setup required.

UncategorizedID: K-Dense-AI/claude-scientific-skills/rowan

Install this agent skill to your local

pnpm dlx add-skill https://github.com/K-Dense-AI/claude-scientific-skills/tree/HEAD/scientific-skills/rowan

Skill Files

Browse the full folder contents for rowan.

Download Skill

Loading file tree…

scientific-skills/rowan/SKILL.md

Skill Metadata

Name
rowan
Description
Rowan is a cloud-native molecular modeling and medicinal-chemistry workflow platform with a Python API. Use for pKa and macropKa prediction, conformer and tautomer ensembles, docking and analogue docking, protein-ligand cofolding, MSA generation, molecular dynamics, permeability, descriptor workflows, and related small-molecule or protein modeling tasks. Ideal for programmatic batch screening, multi-step chemistry pipelines, and workflows that would otherwise require maintaining local HPC/GPU infrastructure.

Rowan: Cloud-Native Molecular-Modeling and Drug-Design Workflows

Overview

Rowan is a cloud-native workflow platform for molecular simulation, medicinal chemistry, and structure-based design. Its Python API exposes a unified interface for small-molecule modeling, property prediction, docking, molecular dynamics, and AI structure workflows.

Use Rowan when you want to run medicinal-chemistry or molecular-design workflows programmatically without maintaining local HPC infrastructure, GPU provisioning, or a collection of separate modeling tools. Rowan handles all infrastructure, result management, and computation scaling.

When to use Rowan

Rowan is a good fit for:

  • Quantum chemistry, semiempirical methods, or neural network potentials
  • Batch property prediction (pKa, descriptors, permeability, solubility)
  • Conformer and tautomer ensemble generation
  • Docking workflows (single-ligand, analogue series, pose refinement)
  • Protein-ligand cofolding and MSA generation
  • Multi-step chemistry pipelines (e.g., tautomer search → docking → pose analysis)
  • Batch medicinal-chemistry campaigns where you need consistent, scalable infrastructure

Rowan is not the right fit for:

  • Simple molecular I/O (use RDKit directly)
  • Post-HF ab initio quantum chemistry or relativistic calculations

Access and pricing model

Rowan uses a credit-based usage model. All users, including free-tier users, can create API keys and use the Python API.

Free-tier access

  • Access to all Rowan core workflows
  • 20 credits per week
  • 500 signup credits

Pricing and credit consumption

Credits are consumed according to compute type:

  • CPU: 1 credit per minute
  • GPU: 3 credits per minute
  • H100/H200 GPU: 7 credits per minute

Purchased credits are priced per credit and remain valid for up to one year from purchase.

Typical cost estimates

| Workflow | Typical Runtime | Estimated Credits | Notes | |----------|----------------|-------------------|-------| | Descriptors | <1 min | 0.5–2 | Lightweight, good for triage | | pKa (single transition) | 2–5 min | 2–5 | Depends on molecule size | | MacropKa (pH 0–14) | 5–15 min | 5–15 | Broader sampling, higher cost | | Conformer search | 3–10 min | 3–10 | Ensemble quality matters | | Tautomer search | 2–5 min | 2–5 | Heterocyclic systems | | Docking (single ligand) | 5–20 min | 5–20 | Depends on pocket size, refinement | | Analogue docking series (10–50 ligands) | 30–120 min | 30–100+ | Shared reference frame | | MSA generation | 5–30 min | 5–30 | Sequence length dependent | | Protein-ligand cofolding | 15–60 min | 20–50+ | AI structure prediction, GPU-heavy |

Quick start

uv pip install rowan-python
import rowan
rowan.api_key = "your_api_key_here"  # or set ROWAN_API_KEY env var

# Submit a descriptors workflow — completes in under a minute
wf = rowan.submit_descriptors_workflow("CC(=O)Oc1ccccc1C(=O)O", name="aspirin")
result = wf.result()

print(result.descriptors['MW'])    # 180.16
print(result.descriptors['SLogP']) # 1.19
print(result.descriptors['TPSA'])  # 59.44

If that prints without error, you're set up correctly.

Installation

uv pip install rowan-python
# or: pip install rowan-python

User and webhook management

Authentication

Set an API key via environment variable (recommended):

export ROWAN_API_KEY="your_api_key_here"

Or set directly in Python:

import rowan
rowan.api_key = "your_api_key_here"

Verify authentication:

import rowan
user = rowan.whoami()  # Returns user info if authenticated
print(f"User: {user.email}")
print(f"Credits available: {user.credits_available_string}")

Webhook secret management

For webhook signature verification, manage secrets through your user account:

import rowan

# Get your current webhook secret (returns None if none exists)
secret = rowan.get_webhook_secret()
if secret is None:
    secret = rowan.create_webhook_secret()
print(f"Secret key: {secret.secret}")

# Rotate your secret (invalidates old, creates new)
# Use this periodically for security
new_secret = rowan.rotate_webhook_secret()
print(f"New secret created (old secret disabled): {new_secret.secret}")

# Verify incoming webhook signatures
is_valid = rowan.verify_webhook_secret(
    request_body=b"...",           # Raw request body (bytes)
    signature="X-Rowan-Signature", # From request header
    secret=secret.secret
)

Molecule input formats

Rowan accepts molecules in the following formats:

  • SMILES (preferred): "CCO", "c1ccccc1O"
  • SMARTS patterns (for some workflows): subset of SMARTS for substructure matching
  • InChI (if supported in your API version): "InChI=1S/C2H6O/c1-2-3/h3H,2H2,1H3"

The API will validate input and raise a rowan.ValidationError if a molecule cannot be parsed. Always use canonicalized SMILES for reproducibility.

Tip: Use RDKit to validate SMILES before submission:

from rdkit import Chem
smiles = "CCO"
mol = Chem.MolFromSmiles(smiles)
if mol is None:
    raise ValueError(f"Invalid SMILES: {smiles}")

Core usage pattern

Most Rowan tasks follow the same three-step pattern:

  1. Submit a workflow
  2. Wait for completion (with optional streaming)
  3. Retrieve typed results with convenience properties
import rowan

# 1. Submit — use the specific workflow function (not the generic submit_workflow)
workflow = rowan.submit_descriptors_workflow(
    "CC(=O)Oc1ccccc1C(=O)O",
    name="aspirin descriptors",
)

# 2. & 3. Wait and retrieve
result = workflow.result()  # Blocks until done (default: wait=True, poll_interval=5)
print(result.data)              # Raw dict
print(result.descriptors['MW']) # 180.16 — use result.descriptors dict, not result.molecular_weight

For long-running workflows, use streaming:

for partial in workflow.stream_result(poll_interval=5):
    print(f"Progress: {partial.complete}%")
    print(partial.data)

result() vs. stream_result()

| Pattern | Use When | Duration | |---------|----------|----------| | result() | You can wait for the full result | <5 min typical | | stream_result() | You want progress feedback or need early partial results | >5 min, or interactive use |

Guideline: Use result() for descriptors, pKa. Use stream_result() for conformer search, docking, cofolding.

Working with results

Rowan's API includes typed workflow result objects with convenience properties.

Using typed properties and .data

Results have two access patterns:

  1. Convenience properties (recommended first): result.descriptors, result.best_pose, result.conformer_energies
  2. Raw fallback: result.data — raw dictionary from the API

Example:

result = rowan.submit_descriptors_workflow(
    "CCO",
    name="ethanol",
).result()

# Convenience property (returns dict of all descriptors):
print(result.descriptors['MW'])   # 46.042
print(result.descriptors['SLogP'])  # -0.001
print(result.descriptors['TPSA'])   # 57.96

# Raw data fallback (descriptors are nested under 'descriptors' key):
print(result.data['descriptors'])
# {'MW': 46.042, 'SLogP': -0.001, 'TPSA': 57.96, 'nHBDon': 1.0, 'nHBAcc': 1.0, ...}

Note: DescriptorsResult does not have a molecular_weight property. Descriptor keys use short names (MW, SLogP, nHBDon) not verbose names.

Cache invalidation

Some result properties are lazily loaded (e.g., conformer geometries, protein structures). To refresh:

result.clear_cache()
new_structures = result.conformer_molecules  # Refetched

Projects, folders, and organization

For nontrivial campaigns, use projects and folders to keep work organized.

Projects

import rowan

# Create a project
project = rowan.create_project(name="CDK2 lead optimization")
rowan.set_project("CDK2 lead optimization")

# All subsequent workflows go into this project
wf = rowan.submit_descriptors_workflow("CCO", name="test compound")

# Retrieve later
project = rowan.retrieve_project("CDK2 lead optimization")
workflows = rowan.list_workflows(project=project, size=50)

Folders

# Create a hierarchical folder structure
folder = rowan.create_folder(name="docking/batch_1/screening")

wf = rowan.submit_docking_workflow(
    # ... docking params ...
    folder=folder,
    name="compound_001",
)

# List workflows in a folder
results = rowan.list_workflows(folder=folder)

Workflow decision trees

pKa vs. MacropKa

Use microscopic pKa when:

  • You need the pKa of a single ionizable group
  • You're interested in acid–base transitions and protonation thermodynamics
  • The molecule has one or two ionizable sites
  • Speed is critical (faster, fewer credits)

Use macropKa when:

  • You need pH-dependent behavior across a physiologically relevant range (e.g., 0–14)
  • You want aggregated charge and protonation-state populations across pH
  • The molecule has multiple ionizable groups with coupled protonation
  • You need downstream properties like aqueous solubility at different pH

Example decision:

Phenol (pKa ~10): Use microscopic pKa
Amine (pKa ~9–10): Use microscopic pKa
Multi-ionizable drug (N, O, acidic group): Use macropKa
ADME assessment across GI pH: Use macropKa

Conformer search vs. tautomer search

Use conformer search when:

  • A single tautomeric form is known
  • You need a diverse 3D ensemble for docking, MD, or SAR analysis
  • Rotatable bonds dominate the chemical space

Use tautomer search when:

  • Tautomeric equilibrium is uncertain (e.g., heterocycles, keto–enol systems)
  • You need to model all relevant protonation isomers
  • Downstream calculations (docking, pKa) depend on tautomeric form

Combined workflow:

# Step 1: Find best tautomer
taut_wf = rowan.submit_tautomer_search_workflow(
    initial_molecule="O=c1[nH]ccnc1",
    name="imidazole tautomers",
)
best_taut = taut_wf.result().best_tautomer

# Step 2: Generate conformers from best tautomer
conf_wf = rowan.submit_conformer_search_workflow(
    initial_molecule=best_taut,
    name="imidazole conformers",
)

Docking vs. analogue docking vs. cofolding

| Workflow | Use When | Input | Output | |----------|----------|-------|--------| | Docking | Single ligand, known pocket | Protein + SMILES + pocket coords | Pose, score, dG | | Analogue docking | 5–100+ related compounds | Protein + SMILES list + reference ligand | All poses, reference-aligned | | Protein-ligand cofolding | Sequence + ligand, no crystal structure | Protein sequence + SMILES | ML-predicted bound complex |

Common workflow categories

1. Descriptors

A lightweight entry point for batch triage, SAR, or exploratory scripts.

wf = rowan.submit_descriptors_workflow(
    "CC(=O)Oc1ccccc1C(=O)O",  # positional arg, accepts SMILES string
    name="aspirin descriptors",
)

result = wf.result()
print(result.descriptors['MW'])    # 180.16
print(result.descriptors['SLogP']) # 1.19
print(result.descriptors['TPSA'])  # 59.44
print(result.data['descriptors'])
# {'MW': 180.16, 'SLogP': 1.19, 'TPSA': 59.44, 'nHBDon': 1.0, 'nHBAcc': 4.0, ...}

Common descriptor keys:

| Key | Description | Typical drug range | |-----|-------------|-------------------| | MW | Molecular weight (Da) | <500 (Lipinski) | | SLogP | Calculated LogP (lipophilicity) | -2 to +5 | | TPSA | Topological polar surface area (Ų) | <140 for oral bioavailability | | nHBDon | H-bond donor count | ≤5 (Lipinski) | | nHBAcc | H-bond acceptor count | ≤10 (Lipinski) | | nRot | Rotatable bond count | <10 for oral drugs | | nRing | Ring count | — | | nHeavyAtom | Heavy atom count | — | | FilterItLogS | Estimated aqueous solubility (LogS) | >-4 preferred | | Lipinski | Lipinski Ro5 pass (1.0) or fail (0.0) | — |

The result contains hundreds of additional molecular descriptors (BCUT, GETAWAY, WHIM, etc.); access any via result.descriptors['key'].

2. Microscopic pKa

For protonation-state energetics and acid/base behavior of a specific structure.

Two methods are available:

| Method | Input | Speed | Covers | Use when | |--------|-------|-------|--------|----------| | chemprop_nevolianis2025 | SMILES string | Fast | Deprotonation only (anionic conjugate bases) | Acidic groups only; quick screening | | starling | SMILES string | Fast | Acid + base (full protonation/deprotonation) | Most drug-like molecules; preferred SMILES method | | aimnet2_wagen2024 (default) | 3D molecule object | Slower, higher accuracy | Acid + base | You already have a 3D structure (e.g. from conformer search) |

# Fast path: SMILES input with full acid+base coverage (use starling method when available)
wf = rowan.submit_pka_workflow(
    initial_molecule="c1ccccc1O",       # phenol SMILES; param is initial_molecule, not initial_smiles
    method="starling",   # fast SMILES method, covers acid+base; chemprop_nevolianis2025 is deprotonation-only
    name="phenol pKa",
)

result = wf.result()
print(result.strongest_acid)    # 9.81 (pKa of the most acidic site)
print(result.conjugate_bases)   # list of {pka, smiles, atom_index, ...} per deprotonatable site

3. MacropKa

For pH-dependent protonation behavior across a range.

wf = rowan.submit_macropka_workflow(
    initial_smiles="CN1CCN(CC1)C2=NC=NC3=CC=CC=C32",  # imidazole
    min_pH=0,
    max_pH=14,
    min_charge=-2,  # default
    max_charge=2,   # default
    compute_aqueous_solubility=True,  # default
    name="imidazole macropKa",
)

result = wf.result()
print(result.pka_values)               # list of pKa values
print(result.logd_by_ph)               # dict of {pH: logD}
print(result.aqueous_solubility_by_ph) # dict of {pH: solubility}
print(result.isoelectric_point)        # isoelectric point
print(result.data)
# {'pKa_values': [...], 'logD_by_pH': {...}, 'aqueous_solubility_by_pH': {...}, ...}

4. Conformer search

For 3D ensemble generation when ensemble quality matters.

wf = rowan.submit_conformer_search_workflow(
    initial_molecule="CCOC(=O)N1CCC(CC1)Oc1ncnc2ccccc12",
    num_conformers=50,  # Optional: override default
    name="conformer search",
)

result = wf.result()
print(result.conformer_energies)  # [0.0, 1.2, 2.5, ...]
print(result.conformer_molecules)  # List of 3D molecules
print(result.best_conformer)  # Lowest-energy conformer

5. Tautomer search

For heterocycles and systems where tautomer state affects downstream modeling.

wf = rowan.submit_tautomer_search_workflow(
    initial_molecule="O=c1[nH]ccnc1",  # or keto tautomer
    name="imidazolone tautomers",
)

result = wf.result()
print(result.best_tautomer)  # Most stable SMILES string
print(result.tautomers)      # List of tautomeric SMILES
print(result.molecules)      # List of molecule objects

6. Docking

For protein-ligand docking with optional pose refinement and conformer generation.

# Upload protein once, reuse in multiple workflows
protein = rowan.upload_protein(
    name="CDK2",
    file_path="cdk2.pdb",
)

# Define binding pocket
pocket = {
    "center": [10.5, 24.2, 31.8],
    "size": [18.0, 18.0, 18.0],
}

# Submit docking
wf = rowan.submit_docking_workflow(
    protein=protein,
    pocket=pocket,
    initial_molecule="CCNc1ncc(c(Nc2ccc(F)cc2)n1)-c1cccnc1",
    do_pose_refinement=True,
    do_conformer_search=True,
    name="lead docking",
)

result = wf.result()
print(result.scores)  # Docking scores (kcal/mol)
print(result.best_pose)  # Mol object with 3D coordinates
print(result.data)  # Raw result dict

Protein preparation tips:

  • PDB files should be reasonably clean (remove water/heteroatoms unless intended)
  • Use the same protein object across a docking series for consistency
  • If you have a PDB ID, use rowan.create_protein_from_pdb_id() instead

7. Analogue docking

For placing a compound series into a shared binding context.

# Analogue series (e.g., SAR campaign)
analogues = [
    "CCNc1ncc(c(Nc2ccc(F)cc2)n1)-c1cccnc1",    # reference
    "CCNc1ncc(c(Nc2ccc(Cl)cc2)n1)-c1cccnc1",   # chloro
    "CCNc1ncc(c(Nc2ccc(OC)cc2)n1)-c1cccnc1",   # methoxy
    "CCNc1ncc(c(Nc2cc(C)c(F)cc2)n1)-c1cccnc1", # methyl, fluoro
]

wf = rowan.submit_analogue_docking_workflow(
    analogues=analogues,
    initial_molecule=analogues[0],  # Reference ligand
    protein=protein,
    pocket=pocket,
    name="SAR series docking",
)

result = wf.result()
print(result.analogue_scores)  # List of scores for each analogue
print(result.best_poses)  # List of poses

8. MSA generation

For multiple-sequence alignment (useful for downstream cofolding).

wf = rowan.submit_msa_workflow(
    initial_protein_sequences=[
        "MENFQKVEKIGEGTYGVVYKARNKLTGEVVALKKIRLDTETEGVP"
    ],
    output_formats=["colabfold", "chai", "boltz"],
    name="target MSA",
)

result = wf.result()
result.download_files()  # Downloads alignments to disk

9. Protein-ligand cofolding

For AI-based bound-complex prediction when no crystal structure is available.

wf = rowan.submit_protein_cofolding_workflow(
    initial_protein_sequences=[
        "MENFQKVEKIGEGTYGVVYKARNKLTGEVVALKKIRLDTETEGVP"
    ],
    initial_smiles_list=[
        "CCNc1ncc(c(Nc2ccc(F)cc2)n1)-c1cccnc1"
    ],
    name="protein-ligand cofolding",
)

result = wf.result()
print(result.predictions)  # List of predicted structures
print(result.messages)  # Model metadata/warnings

predicted_structure = result.get_predicted_structure()
predicted_structure.write("predicted_complex.pdb")

All supported workflow types

All workflows follow the same submit → wait → retrieve pattern and support webhooks and project/folder organization.

Core molecular modeling workflows

| Workflow | Function | When to use | |----------|----------|-------------| | Descriptors | submit_descriptors_workflow | First-pass triage: MW, LogP, TPSA, HBA/HBD, Lipinski filter | | pKa | submit_pka_workflow | Single ionizable group; need protonation thermodynamics | | MacropKa | submit_macropka_workflow | Multi-ionizable drugs; pH-dependent charge/LogD/solubility | | Conformer Search | submit_conformer_search_workflow | 3D ensemble for docking, MD, or SAR; known tautomer | | Tautomer Search | submit_tautomer_search_workflow | Heterocycles, keto–enol; uncertain tautomeric form | | Solubility | submit_solubility_workflow | Aqueous or solvent-specific solubility prediction | | Membrane Permeability | submit_membrane_permeability_workflow | Caco-2, PAMPA, BBB, plasma permeability | | ADMET | submit_admet_workflow | Broad drug-likeness and ADMET property sweep |

Structure-based design workflows

| Workflow | Function | When to use | |----------|----------|-------------| | Docking | submit_docking_workflow | Single ligand, known binding pocket | | Analogue Docking | submit_analogue_docking_workflow | SAR series (5–100+ compounds) in a shared pocket | | Batch Docking | submit_batch_docking_workflow | Fast library screening; large compound sets | | Protein MD | submit_protein_md_workflow | Long-timescale dynamics; conformational sampling | | Pose Analysis MD | submit_pose_analysis_md_workflow | MD refinement of a docking pose | | Protein Cofolding | submit_protein_cofolding_workflow | No crystal structure; AI-predicted bound complex | | Protein Binder Design | submit_protein_binder_design_workflow | De novo binder generation against a protein target |

Advanced computational chemistry

| Workflow | Function | When to use | |----------|----------|-------------| | Basic Calculation | submit_basic_calculation_workflow | QM/ML geometry optimization or single-point energy | | Electronic Properties | submit_electronic_properties_workflow | Dipole, partial charges, HOMO-LUMO, ESP | | BDE | submit_bde_workflow | Bond dissociation energies; metabolic soft-spot prediction | | Redox Potential | submit_redox_potential_workflow | Oxidation/reduction potentials | | Spin States | submit_spin_states_workflow | Spin-state energy ordering for organometallics/radicals | | Strain | submit_strain_workflow | Conformational strain relative to global minimum | | Scan | submit_scan_workflow | PES scans; torsion profiles | | Multistage Optimization | submit_multistage_opt_workflow | Progressive optimization across levels of theory |

Reaction chemistry

| Workflow | Function | When to use | |----------|----------|-------------| | Double-Ended TS Search | submit_double_ended_ts_search_workflow | Transition state between two known structures | | IRC | submit_irc_workflow | Confirm TS connectivity; intrinsic reaction coordinate |

Advanced properties

| Workflow | Function | When to use | |----------|----------|-------------| | NMR | submit_nmr_workflow | Predicted 1H/13C chemical shifts for structure verification | | Ion Mobility | submit_ion_mobility_workflow | Collision cross-section (CCS) for MS method development | | Hydrogen Bond Strength | submit_hydrogen_bond_basicity_workflow | H-bond donor/acceptor strength for formulation/solubility | | Fukui | submit_fukui_workflow | Site reactivity indices for electrophilic/nucleophilic attack | | Interaction Energy Decomposition | submit_interaction_energy_decomposition_workflow | Fragment-level interaction analysis |

Binding free energy

| Workflow | Function | When to use | |----------|----------|-------------| | RBFE/FEP | submit_relative_binding_free_energy_perturbation_workflow | Relative ΔΔG for congeneric series | | RBFE Graph | submit_rbfe_graph_workflow | Build and optimize an RBFE perturbation network |

Sequence and structural biology

| Workflow | Function | When to use | |----------|----------|-------------| | MSA | submit_msa_workflow | Multiple sequence alignment for cofolding (ColabFold, Chai, Boltz) | | Solvent-Dependent Conformers | submit_solvent_dependent_conformers_workflow | Solvation-aware conformer ensembles |

Batch submission and retrieval

For libraries or analogue series, submit in a loop using the specific workflow function. The generic rowan.batch_submit_workflow() and rowan.submit_workflow() functions currently return 422 errors from the API — use the named functions (submit_descriptors_workflow, submit_pka_workflow, etc.) instead.

Submit a batch

smileses = ["CCO", "CC(=O)O", "c1ccccc1O"]
names = ["ethanol", "acetic acid", "phenol"]

workflows = [
    rowan.submit_descriptors_workflow(smi, name=name)
    for smi, name in zip(smileses, names)
]

print(f"Submitted {len(workflows)} workflows")

Poll batch status

statuses = rowan.batch_poll_status([wf.uuid for wf in workflows])
# Returns aggregate counts — not per-UUID:
# {'queued': 0, 'running': 1, 'complete': 2, 'failed': 0, 'total': 3, ...}

if statuses["complete"] == statuses["total"]:
    print("All workflows done")
elif statuses["failed"] > 0:
    print(f"{statuses['failed']} workflows failed")

Retrieve and collect results

results = []
for wf in workflows:
    try:
        result = wf.result()
        results.append(result.data)
    except rowan.WorkflowError as e:
        print(f"Workflow {wf.uuid} failed: {e}")

# Optionally aggregate into DataFrame
import pandas as pd
df = pd.DataFrame(results)

Non-blocking / fire-and-check pattern

For long-running workflows where you don't want to hold a process open, submit workflows, save their UUIDs, and check back later in a separate process.

Session 1 — submit and save UUIDs:

import rowan, json

rowan.api_key = "..."
smileses = ["CCO", "CC(=O)O", "c1ccccc1O"]

workflows = [
    rowan.submit_descriptors_workflow(smi, name=f"compound_{i}")
    for i, smi in enumerate(smileses)
]

# Save UUIDs to disk (or a database)
uuids = [wf.uuid for wf in workflows]
with open("workflow_uuids.json", "w") as f:
    json.dump(uuids, f)

print("Submitted. Check back later.")

Session 2 — check status and collect results when ready:

import rowan, json

rowan.api_key = "..."

with open("workflow_uuids.json") as f:
    uuids = json.load(f)

results = []
for uuid in uuids:
    wf = rowan.retrieve_workflow(uuid)
    if wf.done():
        result = wf.result(wait=False)
        results.append({"uuid": uuid, "data": result.data})
    else:
        print(f"{uuid}: still running ({wf.status})")

print(f"Collected {len(results)} completed results")

Webhooks and asynchronous workflows

For long-running campaigns or when you don't want to keep a process alive, use webhooks to notify your backend when workflows complete.

Setting up webhooks

Every workflow submission function accepts a webhook_url parameter:

wf = rowan.submit_docking_workflow(
    protein=protein,
    pocket=pocket,
    initial_molecule="CCO",
    webhook_url="https://myserver.com/rowan_callback",
    name="docking with webhook",
)

print(f"Workflow submitted. Result will be POSTed to webhook when complete.")

Webhook URLs can be passed to any specific workflow function (submit_docking_workflow(), submit_pka_workflow(), submit_descriptors_workflow(), etc.).

Webhook authentication with secrets

Rowan supports webhook signature verification to ensure requests are authentic. You'll need to:

  1. Create or retrieve a webhook secret:
import rowan

# Create a new webhook secret
secret = rowan.create_webhook_secret()
print(f"Your webhook secret: {secret.secret}")

# Or retrieve an existing secret
secret = rowan.get_webhook_secret()

# Rotate your secret (invalidates old one, creates new)
new_secret = rowan.rotate_webhook_secret()
  1. Verify incoming webhook requests:
import rowan
import hmac
import json

def verify_webhook(request_body: bytes, signature: str, secret: str) -> bool:
    """Verify the HMAC-SHA256 signature of a webhook request."""
    return rowan.verify_webhook_secret(request_body, signature, secret)

Webhook payload and signature

When a workflow completes, Rowan POSTs a JSON payload to your webhook URL with the header:

X-Rowan-Signature: <HMAC-SHA256 signature>

The request body contains the complete workflow result:

{
  "workflow_uuid": "wf_12345abc",
  "workflow_type": "docking",
  "workflow_name": "lead docking",
  "status": "COMPLETED_OK",
  "created_at": "2025-04-01T12:00:00Z",
  "completed_at": "2025-04-01T12:15:30Z",
  "data": {
    "scores": [-8.2, -8.0, -7.9],
    "best_pose": {...},
    "metadata": {...}
  }
}

Example webhook handler with signature verification (FastAPI)

from fastapi import FastAPI, Request, HTTPException
import rowan
import json

app = FastAPI()
_ws = rowan.get_webhook_secret() or rowan.create_webhook_secret()
webhook_secret = _ws.secret

@app.post("/rowan_callback")
async def handle_rowan_webhook(request: Request):
    # Get request body and signature
    body = await request.body()
    signature = request.headers.get("X-Rowan-Signature")

    if not signature:
        raise HTTPException(status_code=400, detail="Missing X-Rowan-Signature header")

    # Verify signature
    if not rowan.verify_webhook_secret(body, signature, webhook_secret):
        raise HTTPException(status_code=401, detail="Invalid webhook signature")

    # Parse and process
    payload = json.loads(body)
    wf_uuid = payload["workflow_uuid"]
    status = payload["status"]

    if status == "COMPLETED_OK":
        print(f"Workflow {wf_uuid} succeeded!")
        result_data = payload["data"]
        # Process result, update database, trigger next workflow, etc.
    elif status == "FAILED":
        print(f"Workflow {wf_uuid} failed!")
        # Handle failure

    # Respond quickly to prevent retries
    return {"status": "received"}

Webhook best practices

  • Always verify signatures using rowan.verify_webhook_secret() to ensure requests are from Rowan
  • Respond quickly (< 5 seconds); offload heavy processing to async tasks or background jobs
  • Implement idempotency: workflows may retry; handle duplicate payloads gracefully using workflow_uuid
  • Log all events for debugging and audit trails
  • Use for long campaigns: webhooks shine with 50+ workflows; for small jobs, polling with result() is simpler
  • Rotate secrets regularly using rowan.rotate_webhook_secret() for security
  • Return 2xx status to confirm receipt; Rowan may retry on 5xx errors

Protein utilities

Upload proteins

# From local PDB file
protein = rowan.upload_protein(
    name="egfr_kinase_domain",
    file_path="egfr_kinase.pdb",
)

# From PDB database
protein_from_pdb = rowan.create_protein_from_pdb_id(
    name="CDK2 (1M17)",
    code="1M17",
)

# Retrieve previously uploaded protein
protein = rowan.retrieve_protein("protein-uuid")

# List all proteins
my_proteins = rowan.list_proteins()

Protein preparation guidance

  • File format: PDB, mmCIF (Rowan auto-detects)
  • Water molecules: Rowan usually keeps relevant water; remove bulk water beforehand if desired
  • Heteroatoms: Cofactors, ions, and bound ligands are usually preserved; remove unwanted heteroatoms before upload
  • Multi-chain proteins: Fully supported
  • Resolution: Works with NMR structures, homology models, and cryo-EM; quality matters for downstream predictions
  • Validation: Rowan validates PDB syntax; severely malformed files may be rejected

End-to-end example: Lead optimization campaign

This example demonstrates a realistic workflow for optimizing a hit compound:

import rowan
import pandas as pd

# 1. Create a project and folder for organization
project = rowan.create_project(name="CDK2 Hit Optimization")
rowan.set_project("CDK2 Hit Optimization")
folder = rowan.create_folder(name="round_1_tautomers_and_pka")

# 2. Load hit compound and analogues
hit = "CCNc1ncc(c(Nc2ccc(F)cc2)n1)-c1cccnc1"  # Known hit
analogues = [
    "CCNc1ncc(c(Nc2ccccc2)n1)-c1cccnc1",      # Remove F
    "CCNc1ncc(c(Nc2ccc(Cl)cc2)n1)-c1cccnc1",  # Cl instead of F
    "CCC(C)Nc1ncc(c(Nc2ccc(F)cc2)n1)-c1cccnc1",  # Propyl instead of ethyl
]

# 3. Determine best tautomers (just in case)
print("Searching tautomeric forms...")
taut_workflows = [
    rowan.submit_tautomer_search_workflow(
        smi, name=f"analog_{i}", folder=folder,
    )
    for i, smi in enumerate(analogues)
]

best_tautomers = []
for wf in taut_workflows:
    result = wf.result()
    best_tautomers.append(result.best_tautomer)

# 4. Predict pKa and basic properties for all analogues
print("Predicting pKa and properties...")
pka_workflows = [
    rowan.submit_pka_workflow(
        smi, method="chemprop_nevolianis2025", name=f"pka_{i}", folder=folder,
    )
    for i, smi in enumerate(best_tautomers)
]

descriptor_workflows = [
    rowan.submit_descriptors_workflow(smi, name=f"desc_{i}", folder=folder)
    for i, smi in enumerate(best_tautomers)
]

# 5. Collect results
pka_results = []
for wf in pka_workflows:
    try:
        result = wf.result()
        pka_results.append({
            "compound": wf.name,
            "pka": result.strongest_acid,  # pKa of the strongest acid site
            "uuid": wf.uuid,
        })
    except rowan.WorkflowError as e:
        print(f"pKa prediction failed for {wf.name}: {e}")

descriptor_results = []
for wf in descriptor_workflows:
    try:
        result = wf.result()
        desc = result.descriptors
        descriptor_results.append({
            "compound": wf.name,
            "mw": desc.get("MW"),
            "logp": desc.get("SLogP"),
            "hba": desc.get("nHBAcc"),
            "hbd": desc.get("nHBDon"),
            "uuid": wf.uuid,
        })
    except rowan.WorkflowError as e:
        print(f"Descriptor calculation failed for {wf.name}: {e}")

# 6. Merge and summarize
df_pka = pd.DataFrame(pka_results)
df_desc = pd.DataFrame(descriptor_results)
df = df_pka.merge(df_desc, on="compound", how="outer")

print("\n=== Preliminary SAR ===")
print(df.to_string())

# 7. Select promising compound for docking
# compound names are "pka_0", "pka_1", etc. — extract index to look up SMILES
top_idx = int(df.loc[df["pka"].idxmin(), "compound"].split("_")[1])
top_smiles = best_tautomers[top_idx]

print(f"\nProceeding with docking: {top_smiles}")

# 8. Docking campaign
protein = rowan.create_protein_from_pdb_id(name="CDK2_1CKP", code="1CKP")
pocket = {"center": [10.5, 24.2, 31.8], "size": [18.0, 18.0, 18.0]}

docking_wf = rowan.submit_docking_workflow(
    protein=protein,
    pocket=pocket,
    initial_molecule=top_smiles,
    do_pose_refinement=True,
    name=f"docking_{top_compound}",
)

dock_result = docking_wf.result()
print(f"\nDocking score: {dock_result.scores[0]:.2f} kcal/mol")
print(f"Best pose saved to: best_pose.pdb")
dock_result.best_pose.write("best_pose.pdb")

Error handling and troubleshooting

Common errors and solutions

import rowan

# Error 1: Invalid SMILES
try:
    wf = rowan.submit_descriptors_workflow("CCCC(CC", name="bad smiles")  # Invalid
except rowan.ValidationError as e:
    print(f"Invalid SMILES: {e}")
    # Solution: Use RDKit to validate before submission
    from rdkit import Chem
    smi = Chem.MolToSmiles(Chem.MolFromSmiles(smi))

# Error 2: API key not set
try:
    wf = rowan.submit_descriptors_workflow("CCO")
except rowan.AuthenticationError:
    print("API key not found. Set ROWAN_API_KEY env var or call rowan.api_key = '...'")

# Error 3: Insufficient credits
try:
    wf = rowan.submit_protein_cofolding_workflow(...)
except rowan.InsufficientCreditsError as e:
    print(f"Not enough credits: {e}. Purchase more or reduce job size.")

# Error 4: Workflow failed (bad molecule, etc.)
try:
    wf = rowan.submit_docking_workflow(...)
    result = wf.result()
except rowan.WorkflowError as e:
    print(f"Workflow failed: {e}")
    # Check wf.status for details
    print(f"Status: {wf.status}")

# Error 5: Workflow not yet done — poll manually
result = wf.result(wait=True, poll_interval=5)  # waits and polls every 5s
# Or check status without blocking:
if not wf.done():
    print("Workflow still running. Call wf.result() again later.")

Debugging tips

  • Check workflow status: wf.status, check wf.done(), or call wf.get_status()
  • Inspect raw result: result.data instead of convenience properties
  • Re-run failed workflow: Save UUIDs and retry with rowan.retrieve_workflow(uuid)
  • Validate molecules beforehand: Use RDKit or Chemaxon before batch submission

Recommended usage patterns

  • Prefer Rowan-native workflows over low-level assembly when they exist
  • Use projects and folders for any nontrivial campaign (>5 workflows)
  • Use result() to block until complete (default: wait=True, poll_interval=5)
  • Use typed result properties first, fall back to .data for unmapped fields
  • Use batch submission for compound libraries or analogue series
  • Chain workflows for multi-step chemistry campaigns:
    • pKa → macropKa → permeability (ADME assessment)
    • tautomer search → docking → pose-analysis MD (pose refinement)
    • MSA generation → protein-ligand cofolding (AI structure prediction)
  • Use webhooks for long-running campaigns (>50 workflows) or asynchronous pipelines
  • Use streaming for interactive feedback on large conformer/docking searches

Summary

Use Rowan when your workflow requires cloud execution for molecular-design tasks, especially when you want one unified API and consistent result handling across small-molecule modeling, proteins, docking, ADME prediction, and ML structure generation.

Rowan is a molecular-design workflow platform, not just a remote chemistry engine. It handles infrastructure scaling, result persistence, and multi-step pipeline orchestration so you can focus on science.