Design proteins
Generate binder, generic, or composite protein designs, migrate legacy callers safely, and understand the discriminated result shapes.
POST /compute/v1/protein/design is a legacy-compatible union. New integrations should send the top-level type discriminator: binder for target-plus-binder design or generic for free-form protein design without target semantics. The legacy { target, binder_specification, num_proteins } body is still accepted for migration.
Type-discriminated request bodies
Section titled “Type-discriminated request bodies”New callers should start with one of the type-discriminated request branches below:
{
"type": "binder",
"num_proteins": 25,
"templates": [
{ "id": "complex", "type": "url", "url": "https://example.com/target-and-binder.cif" }
],
"target": {
"entities": [
{
"type": "from_template",
"template_id": "complex",
"chain_id": "A",
"crop_residues": "all",
"epitope_residues": [42, 43, 44],
"non_binding_residues": [0, 1, 2]
}
]
# optional: "bonds" (see Core Concepts)
},
"binder": {
"modality": "nanobody",
"entities": [
{
"type": "from_template",
"template_id": "complex",
"chain_id": "B",
"crop_residues": "all",
"design_motifs": [
{
"type": "replacement",
"start_index": 26,
"end_index": 33,
"design_length_range": { "min": 7, "max": 10 },
"filters": [
{ "type": "max_hydrophobic_fraction", "max_fraction": 0.35 }
]
}
]
}
]
}
# omit "global_design_filters" to keep the default excluded_amino_acids:["C"]
}templatesis a request-local CIF catalog. Reuse the sametemplate_idacrosstargetandbinderwhen one uploaded CIF contains both.global_design_filtersapplies to every designed region. Omit it to keep the defaultexcluded_amino_acids: ["C"]; pass[]to opt out completely.design_motifs[].filtersstacks withglobal_design_filters, so motif-local filters can tighten only the regions that need extra constraints.from_templateselects a chain fromtemplates;no_templateretains the compact legacy entity contract, includingdesigned_protein.valuestrings such as"MKT5..8G".- Motif indices are 0-based positions within the selected
crop_residues, while target annotations and bond residue indices refer to positions in the original CIF chain.
Binder mode (type: "binder")
Section titled “Binder mode (type: "binder")”binder mode keeps target context separate from the designed binder:
target.entitiesis fixed context. Each target entity can come from a template or from the template-free entity union.- A binder with
modalityandentitiesdesigns one binder specification. binder.type: "uniformly_sampled"samples uniformly across multiple custom ortype: "boltz_curated"specifications. This replaces legacyuniformly_sampled_specificationsfor new requests.binder.type: "boltz_curated"lets Boltz select maintained nanobody or antibody scaffold families.
For from_template binder entities, design_motifs is where redesign work happens. Use replacement or insertion motifs, and omit design_motifs on a selected chain to keep it as fixed scaffold context.
Generic mode (type: "generic")
Section titled “Generic mode (type: "generic")”Use generic when you want protein design without target/binder semantics:
- Provide top-level
entities, plus optional covalentbonds. - Mix
from_templateandno_templateentities in the same run. - At least one entity or motif must create actual design work; fixed-only generic requests are rejected.
Generic results still use the same run lifecycle and artifact model, but they omit binding-specific metrics.
Fusion proteins (type: "fusion_protein")
Section titled “Fusion proteins (type: "fusion_protein")”Within a generic request, use fusion_protein to concatenate two or more ordered protein segments into one output chain:
output_chain_idnames the single chain that survives in generated results.segmentsmay mixfrom_template, fixedno_templateproteins, and designedno_templateproteins. Template-free segments omitchain_idsbecause only the parent owns an output chain ID.- Any ordered combination of at least two supported protein segments is valid; templates are optional.
- Segments are appended in array order. The model receives the first segment as the surviving chain and fuses each later segment into it.
- Every segment must be a non-cyclic protein. Cyclic sources and targets are rejected.
- A fusion protein may be fixed when another generic entity supplies design work; the request as a whole must still contain at least one designed entity or motif.
- Multiple fusion proteins may coexist with ordinary generic entities. Top-level
bondscannot reference a fusion output chain.
Fusion results remain type: "generic". Only the parent output_chain_id appears in the fused result entity.
run() submits a design, waits while results are generated, and downloads them to a local directory. The Python workflow helper keeps the existing target + binder_specification convenience signature; the CLI and TypeScript examples below send the type-discriminated union body directly. Use start() + client.experiments.download_results() when you want to submit now and download later.
import osfrom boltz_api import Boltz
client = Boltz( base_url="https://api.boltz.bio", api_key=os.environ["BOLTZ_API_KEY"])
target = {"type": "no_template","entities": [{"type": "protein", "value": "MKTIIALSYIFCLVFA", "chain_ids": ["A"]}],"epitope_residues": {"A": [10, 11, 12]},}
binder_specification = {"type": "no_template","modality": "custom_protein","entities": [{"type": "designed_protein", "chain_ids": ["B"], "value": "10..20"}],}
# The convenience helper keeps the legacy ergonomic signature while the endpoint
# remains migration-compatible.
run_dir = client.protein.design.run(target=target,binder_specification=binder_specification,num_proteins=10,name="my-design",)
design = client.protein.design.start(target=target,binder_specification=binder_specification,num_proteins=10,)run_dir = client.experiments.download_results(id=design.id, name="my-design")Write your type-discriminated request body to protein-design.json (see Type-discriminated request bodies), then:
export BOLTZ_BASE_URL="https://api.boltz.bio"RUN_ID=$( boltz-api --format raw protein:design start \ --input @json://./protein-design.json | jq -r '.id')
boltz-api download-results --id "$RUN_ID" --name my-designThe TypeScript client drives the REST API directly. Submit with start(), then poll and read results yourself.
import Boltz from 'boltz-api'
const client = new Boltz({ baseURL: "https://api.boltz.bio", apiKey: process.env['BOLTZ_API_KEY'] })
const design = await client.protein.design.start({ type: 'binder', num_proteins: 25, templates: [ { id: 'complex', type: 'url', url: 'https://example.com/target-and-binder.cif', }, ], target: { entities: [ { type: 'from_template', template_id: 'complex', chain_id: 'A', crop_residues: 'all', epitope_residues: [42, 43, 44], }, ], }, binder: { modality: 'nanobody', entities: [ { type: 'from_template', template_id: 'complex', chain_id: 'B', crop_residues: 'all', design_motifs: [ { type: 'replacement', start_index: 26, end_index: 33, design_length_range: { min: 7, max: 10 }, filters: [{ type: 'max_hydrophobic_fraction', max_fraction: 0.35 }], }, ], }, ], },})Legacy migration
Section titled “Legacy migration”Use BinderProteinDesignRunInput for binder design or GenericProteinDesignRunInput for generic
protein design. POST /compute/v1/protein/design still accepts the legacy ProteinDesignRunInput
body below while you move callers over to top-level type:
{
"num_proteins": 10,
"target": {
"type": "no_template",
"entities": [
{ "type": "protein", "chain_ids": ["A"], "value": "MKTAYIAKQRQISFVKSHFSRQ" }
],
"epitope_residues": { "A": [10, 11, 12] }
},
"binder_specification": {
"type": "no_template",
"modality": "peptide",
"entities": [
{ "type": "designed_protein", "chain_ids": ["B"], "value": "12..18" }
]
}
}- Legacy requests keep
target,binder_specification,rules, anduniformly_sampled_specifications. - Legacy and type-discriminated
binderandgenericrequests accept10..1_000_000fornum_proteins. - New integrations should prefer the type-discriminated request shapes above.
For fixed single-CIF sequence redesign, use POST /compute/v1/protein/sequence-redesign.
Output format
Section titled “Output format”When you download with run() / start() + client.experiments.download_results() (or the CLI’s download-results), results land in a self-contained run directory:
run() and start() + client.experiments.download_results() poll on your behalf, append each result as it's generated, and download its files into a self-contained run directory. Rerun with the same name to resume.
boltz-experiments/└── my-run/ # the name you chose (or an auto-generated one) ├── .boltz-run.json # run + resume state, managed for you (don't edit) ├── run.json # the run object: status, progress, engine (download URLs stripped) └── results/ ├── index.jsonl # the manifest: one JSON record per result └── <result-id>/ ├── archive.tar.gz # the downloaded result archive ├── metadata.json # this result's fields (metrics, sequence/SMILES, …) └── files/ # extracted from the archive ├── metrics.json ├── <result-id>_predicted.cif # predicted structure └── pae.npzresults/index.jsonl is what you read to triage a run: one compact JSON record per result, appended as results arrive. Each record mirrors the API result minus its artifacts (those are short-lived download URLs), and adds a paths map pointing at the files downloaded for that result. Each record also carries the designed `entities` and discriminated `metrics` for each generated result.
{ "id": "<result-id>", "created_at": "2026-02-25T13:03:40Z", "metrics": { "binding_confidence": 0.94, "structure_confidence": 0.95 }, "paths": { "archive": "results/<result-id>/archive.tar.gz", "files": "results/<result-id>/files", "metrics": "results/<result-id>/files/metrics.json", "structure": "results/<result-id>/files/<result-id>_predicted.cif", "pae": "results/<result-id>/files/pae.npz" }}Everything is downloaded by default. To keep just the manifest and skip the archives, pass download_mode="metadata_only".
download-results polls on your behalf, appends each result as it's generated, and downloads its files into a self-contained run directory. Rerun with the same --name to resume.
boltz-experiments/└── my-run/ # the name you chose (or an auto-generated one) ├── .boltz-run.json # run + resume state, managed for you (don't edit) ├── run.json # the run object: status, progress, engine (download URLs stripped) └── results/ ├── index.jsonl # the manifest: one JSON record per result └── <result-id>/ ├── archive.tar.gz # the downloaded result archive ├── metadata.json # this result's fields (metrics, sequence/SMILES, …) └── files/ # extracted from the archive ├── metrics.json ├── <result-id>_predicted.cif # predicted structure └── pae.npzresults/index.jsonl is what you read to triage a run: one compact JSON record per result, appended as results arrive. Each record mirrors the API result minus its artifacts (those are short-lived download URLs), and adds a paths map pointing at the files downloaded for that result. Each record also carries the designed `entities` and discriminated `metrics` for each generated result.
{ "id": "<result-id>", "created_at": "2026-02-25T13:03:40Z", "metrics": { "binding_confidence": 0.94, "structure_confidence": 0.95 }, "paths": { "archive": "results/<result-id>/archive.tar.gz", "files": "results/<result-id>/files", "metrics": "results/<result-id>/files/metrics.json", "structure": "results/<result-id>/files/<result-id>_predicted.cif", "pae": "results/<result-id>/files/pae.npz" }}Everything is downloaded by default. To keep just the manifest and skip the archives, pass --download-mode metadata_only.
The TypeScript client does not download for you; you read the API objects directly. The result and run-status shapes
are identical to what index.jsonl and run.json mirror on disk.
Type-discriminated list_results() responses use top-level type. Binder runs include binding metrics; generic runs include only structure and secondary-structure metrics. In the composite example, all child components are fused into parent chain A, so only A appears in entities. Legacy migration runs keep the historical result shape until you switch request branches.
{
"data": [
{
"id": "prot_des_result_8f3a2b",
"type": "binder",
"created_at": "2026-02-25T13:03:40Z",
"entities": [
{ "type": "protein", "chain_ids": ["B"], "value": "GSAEELKKLAEELAKQGNSEEVKKLAEKLAQ" }
],
"metrics": {
"binding_confidence": 0.88, # 0-1; confidence that binding occurs
"structure_confidence": 0.91, # 0-1; confidence in the predicted structure
"iptm": 0.86, # 0-1; interface predicted TM-score
"min_interaction_pae": 4.9, # Angstrom; lower is better
"helix_fraction": 0.74,
"sheet_fraction": 0.0,
"loop_fraction": 0.26
},
"artifacts": {
"structure": {
"url": "https://.../structure.cif",
"url_expires_at": "2026-02-25T14:03:40Z"
},
"archive": {
"url": "https://.../archive.tar.gz",
"url_expires_at": "2026-02-25T14:03:40Z"
}
},
"warnings": []
}
],
"has_more": True,
"first_id": "prot_des_result_8f3a2b",
"last_id": "prot_des_result_4ab7e0"
}The run object tracks status and progress. It is what retrieve() returns, and what run.json mirrors:
{
"id": "prot_des_run_8f3a2b",
"status": "running", # pending | running | succeeded | failed | stopped
"progress": {
"total_proteins_to_generate": 100,
"num_proteins_generated": 37,
"latest_result_id": "prot_des_result_8f3a2b"
},
"error": None, # { code, message } once status is "failed"
"pipeline": "boltzprot",
"pipeline_version": "1.0",
"livemode": True, # false for runs created with a test key
"workspace_id": "ws_3a2b",
"created_at": "2026-02-25T12:00:00Z",
"started_at": "2026-02-25T12:00:05Z",
"completed_at": None,
"stopped_at": None,
"data_deleted_at": None
# "input" echoes the submitted request while data is retained; type-discriminated runs include top-level "type"
}