Skill v1.0.0
currentAutomated scan100/100version: "1.0.0"
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. license: Proprietary (API key required) compatibility: Python 3.12+, API key required tags: [scientific-skills, rowan, api, cheminformatics, workflow, devops, python, simulation] metadata: skill-author: Rowan Science trigger-keywords: ["pKa prediction", "molecular docking", "conformer search", "chemistry workflow", "drug discovery", "SMILES", "protein structure", "batch molecular modeling", "cloud chemistry"] -------|----------------|-------------------|-------|
| 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 rowanrowan.api_key = "your_api_key_here" # or set ROWAN_API_KEY env var# Submit a descriptors workflow — completes in under a minutewf = rowan.submit_descriptors_workflow("CC(=O)Oc1ccccc1C(=O)O", name="aspirin")result = wf.result()print(result.descriptors['MW']) # 180.16print(result.descriptors['SLogP']) # 1.19print(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 rowanrowan.api_key = "your_api_key_here"
Verify authentication:
import rowanuser = rowan.whoami() # Returns user info if authenticatedprint(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 securitynew_secret = rowan.rotate_webhook_secret()print(f"New secret created (old secret disabled): {new_secret.secret}")# Verify incoming webhook signaturesis_valid = rowan.verify_webhook_secret(request_body=b"...", # Raw request body (bytes)signature="X-Rowan-Signature", # From request headersecret=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 Chemsmiles = "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:
- Submit a workflow
- Wait for completion (with optional streaming)
- 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 retrieveresult = workflow.result() # Blocks until done (default: wait=True, poll_interval=5)print(result.data) # Raw dictprint(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:
- Convenience properties (recommended first):
result.descriptors,result.best_pose,result.conformer_energies - 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.042print(result.descriptors['SLogP']) # -0.001print(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 projectproject = rowan.create_project(name="CDK2 lead optimization")rowan.set_project("CDK2 lead optimization")# All subsequent workflows go into this projectwf = rowan.submit_descriptors_workflow("CCO", name="test compound")# Retrieve laterproject = rowan.retrieve_project("CDK2 lead optimization")workflows = rowan.list_workflows(project=project, size=50)
Folders
# Create a hierarchical folder structurefolder = rowan.create_folder(name="docking/batch_1/screening")wf = rowan.submit_docking_workflow(# ... docking params ...folder=folder,name="compound_001",)# List workflows in a folderresults = 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 pKaAmine (pKa ~9–10): Use microscopic pKaMulti-ionizable drug (N, O, acidic group): Use macropKaADME 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 tautomertaut_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 tautomerconf_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 stringname="aspirin descriptors",)result = wf.result()print(result.descriptors['MW']) # 180.16print(result.descriptors['SLogP']) # 1.19print(result.descriptors['TPSA']) # 59.44print(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_smilesmethod="starling", # fast SMILES method, covers acid+base; chemprop_nevolianis2025 is deprotonation-onlyname="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", # imidazolemin_pH=0,max_pH=14,min_charge=-2, # defaultmax_charge=2, # defaultcompute_aqueous_solubility=True, # defaultname="imidazole macropKa",)result = wf.result()print(result.pka_values) # list of pKa valuesprint(result.logd_by_ph) # dict of {pH: logD}print(result.aqueous_solubility_by_ph) # dict of {pH: solubility}print(result.isoelectric_point) # isoelectric pointprint(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 defaultname="conformer search",)result = wf.result()print(result.conformer_energies) # [0.0, 1.2, 2.5, ...]print(result.conformer_molecules) # List of 3D moleculesprint(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 tautomername="imidazolone tautomers",)result = wf.result()print(result.best_tautomer) # Most stable SMILES stringprint(result.tautomers) # List of tautomeric SMILESprint(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 workflowsprotein = rowan.upload_protein(name="CDK2",file_path="cdk2.pdb",)# Define binding pocketpocket = {"center": [10.5, 24.2, 31.8],"size": [18.0, 18.0, 18.0],}# Submit dockingwf = 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 coordinatesprint(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 ligandprotein=protein,pocket=pocket,name="SAR series docking",)result = wf.result()print(result.analogue_scores) # List of scores for each analogueprint(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 structuresprint(result.messages) # Model metadata/warningspredicted_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 DataFrameimport pandas as pddf = 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, jsonrowan.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, jsonrowan.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:
- Create or retrieve a webhook secret:
import rowan# Create a new webhook secretsecret = rowan.create_webhook_secret()print(f"Your webhook secret: {secret.secret}")# Or retrieve an existing secretsecret = rowan.get_webhook_secret()# Rotate your secret (invalidates old one, creates new)new_secret = rowan.rotate_webhook_secret()
- Verify incoming webhook requests:
import rowanimport hmacimport jsondef 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, HTTPExceptionimport rowanimport jsonapp = 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 signaturebody = 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 signatureif not rowan.verify_webhook_secret(body, signature, webhook_secret):raise HTTPException(status_code=401, detail="Invalid webhook signature")# Parse and processpayload = 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 retriesreturn {"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 fileprotein = rowan.upload_protein(name="egfr_kinase_domain",file_path="egfr_kinase.pdb",)# From PDB databaseprotein_from_pdb = rowan.create_protein_from_pdb_id(name="CDK2 (1M17)",code="1M17",)# Retrieve previously uploaded proteinprotein = rowan.retrieve_protein("protein-uuid")# List all proteinsmy_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 rowanimport pandas as pd# 1. Create a project and folder for organizationproject = 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 analogueshit = "CCNc1ncc(c(Nc2ccc(F)cc2)n1)-c1cccnc1" # Known hitanalogues = ["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 analoguesprint("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 resultspka_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.descriptorsdescriptor_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 summarizedf_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 SMILEStop_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 campaignprotein = 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 SMILEStry:wf = rowan.submit_descriptors_workflow("CCCC(CC", name="bad smiles") # Invalidexcept rowan.ValidationError as e:print(f"Invalid SMILES: {e}")# Solution: Use RDKit to validate before submissionfrom rdkit import Chemsmi = Chem.MolToSmiles(Chem.MolFromSmiles(smi))# Error 2: API key not settry: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 creditstry: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 detailsprint(f"Status: {wf.status}")# Error 5: Workflow not yet done — poll manuallyresult = 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, checkwf.done(), or callwf.get_status() - Inspect raw result:
result.datainstead 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
.datafor 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.