Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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 in
.claude/skills/mkurman-rowan/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 261% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 341% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 254% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 389% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 369% | 0% |
----|----------------|-------------------|-------| | 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 |
bashuv pip install rowan-python
pythonimport 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.
bashuv pip install rowan-python # or: pip install rowan-python
Set an API key via environment variable (recommended):
bashexport ROWAN_API_KEY="your_api_key_here"
Or set directly in Python:
pythonimport rowan rowan.api_key = "your_api_key_here"
Verify authentication:
pythonimport rowan user = rowan.whoami() # Returns user info if authenticated print(f"User: {user.email}") print(f"Credits available: {user.credits_available_string}")
For webhook signature verification, manage secrets through your user account:
pythonimport 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 )
Rowan accepts molecules in the following formats:
"CCO", "c1ccccc1O""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:
pythonfrom rdkit import Chem smiles = "CCO" mol = Chem.MolFromSmiles(smiles) if mol is None: raise ValueError(f"Invalid SMILES: {smiles}")
Most Rowan tasks follow the same three-step pattern:
pythonimport 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:
pythonfor partial in workflow.stream_result(poll_interval=5): print(f"Progress: {partial.complete}%") print(partial.data)
| 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.
Rowan's API includes typed workflow result objects with convenience properties.
Results have two access patterns:
result.descriptors, result.best_pose, result.conformer_energiesresult.data — raw dictionary from the APIExample:
pythonresult = 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.
Some result properties are lazily loaded (e.g., conformer geometries, protein structures). To refresh:
pythonresult.clear_cache() new_structures = result.conformer_molecules # Refetched
For nontrivial campaigns, use projects and folders to keep work organized.
pythonimport 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)
python# 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)
Use microscopic pKa when:
Use macropKa when:
Example decision:
textPhenol (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
Use conformer search when:
Use tautomer search when:
Combined workflow:
python# 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", )
| 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 |
A lightweight entry point for batch triage, SAR, or exploratory scripts.
pythonwf = 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'].
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) |
python# 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
For pH-dependent protonation behavior across a range.
pythonwf = 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': {...}, ...}
For 3D ensemble generation when ensemble quality matters.
pythonwf = 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
For heterocycles and systems where tautomer state affects downstream modeling.
pythonwf = 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
For protein-ligand docking with optional pose refinement and conformer generation.
python# 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:
rowan.create_protein_from_pdb_id() insteadFor placing a compound series into a shared binding context.
python# 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
For multiple-sequence alignment (useful for downstream cofolding).
pythonwf = 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
For AI-based bound-complex prediction when no crystal structure is available.
pythonwf = 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 workflows follow the same submit → wait → retrieve pattern and support webhooks and project/folder organization.
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
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.
pythonsmileses = ["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")
pythonstatuses = 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")
pythonresults = [] 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)
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:
pythonimport 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:
pythonimport 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")
For long-running campaigns or when you don't want to keep a process alive, use webhooks to notify your backend when workflows complete.
Every workflow submission function accepts a webhook_url parameter:
pythonwf = 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.).
Rowan supports webhook signature verification to ensure requests are authentic. You'll need to:
pythonimport 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()
pythonimport 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)
When a workflow completes, Rowan POSTs a JSON payload to your webhook URL with the header:
textX-Rowan-Signature: <HMAC-SHA256 signature>
The request body contains the complete workflow result:
json{ "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": {...} } }
pythonfrom 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"}
rowan.verify_webhook_secret() to ensure requests are from Rowanworkflow_uuidresult() is simplerrowan.rotate_webhook_secret() for securitypython# 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()
This example demonstrates a realistic workflow for optimizing a hit compound:
pythonimport 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")
pythonimport 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.")
wf.status, check wf.done(), or call wf.get_status()result.data instead of convenience propertiesrowan.retrieve_workflow(uuid)result() to block until complete (default: wait=True, poll_interval=5).data for unmapped fieldspKa → macropKa → permeability (ADME assessment)tautomer search → docking → pose-analysis MD (pose refinement)MSA generation → protein-ligand cofolding (AI structure prediction)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.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 15,956 | 5,788 | -64% | 1 | 1 | 0% | 3,095 | 11,188 | +261% | 0 | 0 | — |
case-02 | fail→pass | 14,202 | 11,105 | -22% | 1 | 1 | 0% | 2,822 | 12,442 | +341% | 0 | 0 | — |
case-22 | pass→pass | 10,759 | 10,572 | -2% | 1 | 1 | 0% | 1,846 | 11,886 | +544% | 0 | 0 | — |
case-03 | fail→pass | 16,749 | 7,340 | -56% | 1 | 1 | 0% | 3,270 | 11,575 | +254% | 0 | 0 | — |
case-04 | fail→pass | 11,310 | 5,363 | -53% | 1 | 1 | 0% | 2,256 | 11,041 | +389% | 0 | 0 | — |
case-05 | fail→pass | 12,773 | 9,504 | -26% | 1 | 1 | 0% | 2,545 | 11,927 | +369% | 0 | 0 | — |
case-06 | fail→pass | 8,985 | 6,514 | -28% | 1 | 1 | 0% | 1,672 | 11,301 | +576% | 0 | 0 | — |
case-07 | fail→pass | 16,949 | 4,124 | -76% | 1 | 1 | 0% | 2,869 | 10,783 | +276% | 0 | 0 | — |
case-08 | fail→pass | 10,003 | 6,157 | -38% | 1 | 1 | 0% | 1,813 | 11,299 | +523% | 0 | 0 | — |
case-09 | fail→pass | 10,468 | 6,732 | -36% | 1 | 1 | 0% | 1,677 | 11,258 | +571% | 0 | 0 | — |
case-10 | fail→pass | 30,497 | 7,162 | -77% | 1 | 1 | 0% | 1,160 | 11,179 | +864% | 0 | 0 | — |
case-11 | fail→pass | 14,333 | 13,153 | -8% | 1 | 1 | 0% | 2,583 | 12,513 | +384% | 0 | 0 | — |
case-12 | fail→pass | 20,733 | 3,972 | -81% | 1 | 1 | 0% | 1,556 | 10,751 | +591% | 0 | 0 | — |
case-13 | fail→pass | 10,094 | 2,569 | -75% | 1 | 1 | 0% | 1,783 | 10,359 | +481% | 0 | 0 | — |
case-14 | fail→pass | 18,860 | 5,033 | -73% | 1 | 1 | 0% | 2,264 | 10,942 | +383% | 0 | 0 | — |
case-15 | fail→pass | 5,611 | 5,466 | -3% | 1 | 1 | 0% | 994 | 10,889 | +995% | 0 | 0 | — |
case-16 | fail→pass | 17,375 | 4,858 | -72% | 1 | 1 | 0% | 3,283 | 10,968 | +234% | 0 | 0 | — |
case-17 | fail→pass | 9,028 | 3,304 | -63% | 1 | 1 | 0% | 1,131 | 10,555 | +833% | 0 | 0 | — |
case-18 | fail→pass | 11,918 | 5,355 | -55% | 1 | 1 | 0% | 2,104 | 10,843 | +415% | 0 | 0 | — |
case-19 | fail→pass | 6,478 | 3,579 | -45% | 1 | 1 | 0% | 1,204 | 10,664 | +786% | 0 | 0 | — |
case-20 | pass→pass | 7,384 | 6,048 | -18% | 1 | 1 | 0% | 1,440 | 11,052 | +668% | 0 | 0 | — |
case-21 | pass→pass | 17,472 | 19,773 | +13% | 1 | 1 | 0% | 3,340 | 13,704 | +310% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 20 counted toward the lift figure. The other 2 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +86 percentage points is the difference between those two pass rates over the 20 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.