API Documentation
Custom Tools: Overview
A version is one build of your tool. Build your source, test a completed version, and publish it when ready. Versions use simple names such as v1, v2, and v3 everywhere.
Tool format
1my-custom-tool/
2├── Dockerfile # Runtime environment and dependencies
3├── run.sh # Starts your tool
4├── config.json # Inputs and resource settings
5└── main.py # Your code; other languages are fine- SDK/API builds require Dockerfile, run.sh, and config.json at the source root. main.py is optional; run.sh can invoke any language.
- The working directory is /app. Scalar inputs arrive as environment variables; file inputs arrive as paths under /app/inputs/.
- Write results to /app/out/.
- Jobs have no network access. Install dependencies during the Docker build, outside /app: uploaded source replaces /app at runtime.
Molecules entity automatically included
Reading which chain is which. An input of type sequence, smiles or pdb arrives as its value, as always: a sequence with its chains joined by :, a SMILES string, or a structure file's path. Next to it your tool also gets /app/inputs/<input name>.json, which lists the input's chains, so it can tell the heavy chain from the light chain, or a binder from its target, without guessing from the order.
1{
2 "schemaVersion": 1,
3 "field": "antibody",
4 "type": "sequence",
5 "source": "molecule",
6 "moleculeId": "3f1c…",
7 "groupId": "9a0b…",
8 "name": null,
9 "value": "EVQLVESGGGLVQ…:DIQMTQSPSSLSA…:MKVLAAGIV…",
10 "file": null,
11 "chains": [
12 {"label": "H", "position": 1, "moleculeType": "protein", "subtype": "heavy_chain", "sequence": "EVQLVESGGGLVQ…", "smiles": null},
13 {"label": "L", "position": 2, "moleculeType": "protein", "subtype": "light_chain", "sequence": "DIQMTQSPSSLSA…", "smiles": null},
14 {"label": "A", "position": 3, "moleculeType": "protein", "subtype": "antigen", "sequence": "MKVLAAGIV…", "smiles": null}
15 ]
16}The same file for a value typed or uploaded with the job (source: "value"):
A typed sequence (heavy:light):
1{
2 "schemaVersion": 1,
3 "field": "antibody",
4 "type": "sequence",
5 "source": "value",
6 "moleculeId": null,
7 "groupId": null,
8 "name": null,
9 "value": "EVQLVESGG:DIQMTQSPS",
10 "file": null,
11 "chains": [
12 {"label": "A", "position": 1, "moleculeType": "protein", "subtype": null, "sequence": "EVQLVESGG", "smiles": null},
13 {"label": "B", "position": 2, "moleculeType": "protein", "subtype": null, "sequence": "DIQMTQSPS", "smiles": null}
14 ]
15}A typed SMILES:
1{
2 "schemaVersion": 1,
3 "field": "ligand",
4 "type": "smiles",
5 "source": "value",
6 "moleculeId": null,
7 "groupId": null,
8 "name": null,
9 "value": "CC(=O)Oc1ccccc1C(=O)O",
10 "file": null,
11 "chains": [
12 {"label": null, "position": 1, "moleculeType": "small_molecule", "subtype": null, "sequence": null, "smiles": "CC(=O)Oc1ccccc1C(=O)O"}
13 ]
14}An uploaded PDB file:
1{
2 "schemaVersion": 1,
3 "field": "complex",
4 "type": "pdb",
5 "source": "value",
6 "moleculeId": null,
7 "groupId": null,
8 "name": null,
9 "value": "/app/inputs/complex.pdb",
10 "file": {"path": "/app/inputs/complex.pdb", "name": "complex.pdb", "fileId": null},
11 "chains": [
12 {"label": "H", "position": 1, "moleculeType": "protein", "subtype": null, "sequence": "EVQLVESGG…", "smiles": null},
13 {"label": "L", "position": 2, "moleculeType": "protein", "subtype": null, "sequence": "DIQMTQSPS…", "smiles": null}
14 ]
15}- Which inputs get it: every input declared in
config.jsoninputswith"type": "sequence","smiles"or"pdb", and the file is named after the input'sname. Inputs that take a list of files ("list": true) don't; useincludeMetadatafor them. - It is written for every such input, whether or not the job saves its outputs to the Molecules database. Chain types (
subtype) come only when the input is a Molecules database molecule. sourceis"molecule"when the input is a molecule from your Molecules database (for example a benchmark row or a molecule picked in a pipeline): the chains carry the molecule's labels and chain types. It is"value"when the input was typed or uploaded with the job: the chains are read from the value. A sequence gives one chain per:-separated part, labelledA,B,Cin order; a SMILES gives onesmall_moleculeentry; a PDB or mmCIF file gives its protein and nucleic acid chains from the first model, under the file's own chain ids.- The chains always follow the value your tool got, in the same order, even if the molecule was edited after the job was submitted.
- Always present:
schemaVersion,field(the input's name),type,source,value(what the environment variable holds) andchains.chainscan be empty when nothing could be read, for example a structure file with only ligands. - Can be
null:moleculeIdandgroupId(molecules only),name(not filled in yet),file(pdbinputs only:{"path", "name", "fileId"}), and for each chainlabel,subtype,sequence(not for a small molecule) andsmiles(small molecules only). moleculeTypeis one ofprotein,nucleic_acid,small_molecule.subtypeis one of the Molecules database's chain types: what the chain is,heavy_chain,light_chain,vh,vl,ligand, or its role,antigen,binder,receptor,target,target_chain,scaffold,designed,predicted,chain. It isnullfor a typed or uploaded value and for a molecule chain with no type.- A molecule's chains in the Molecules API (Get a molecule)
Picking the heavy and light chains in main.py:
1import json
2import sys
3
4info = json.load(open("/app/inputs/antibody.json"))
5chains = info["chains"]
6by_type = {chain["subtype"]: chain for chain in chains if chain["subtype"]}
7heavy, light = by_type.get("heavy_chain"), by_type.get("light_chain")
8if heavy is None or light is None:
9 # A typed sequence has no chain types: fall back to the order it was typed in (heavy:light).
10 if len(chains) < 2:
11 sys.exit(f"antibody needs a heavy and a light chain, got {len(chains)} chain(s)")
12 heavy, light = chains[0], chains[1]
13print(heavy["label"], len(heavy["sequence"]), light["label"], len(light["sequence"]))Building your custom tool
The SDK packages and uploads your files and protects against overwriting concurrent edits. Start with a new tool name and a folder containing Dockerfile, run.sh, and config.json.
1from tamarind import Tamarind
2
3with Tamarind(api_key="YOUR_API_KEY", api_base="https://app.tamarind.bio/api/") as client:
4 tool = client.custom_tools.create("my-custom-tool", display_name="My Custom Tool")
5 build = tool.build("./my-custom-tool")
6 version = build.version.monitor(timeout=3600)
7 # Optional: test version.version with /submit-job before publishing (see below).
8 published_tool = version.publish()
9 print(published_tool.name, published_tool.default_version)Using your custom tool
Once a build is Complete, you can run it with /submit-job before publishing. Use an API key with access to the tool.
1POST https://app.tamarind.bio/api/submit-job
2x-api-key: YOUR_API_KEY
3Content-Type: application/json
4
5{
6 "jobName": "my-custom-tool-test",
7 "type": "my-custom-tool",
8 "version": "v1",
9 "settings": {
10 "sequence": "MKTAYIAKQRQISFVKSHFSRQ"
11 }
12}Set type to your tool's name and version to the completed build's version (for example, v1). Replace settings with the inputs declared in your config.json; this example assumes an input named sequence. Choose a unique jobName for each run.
Publishing selects the default version. After publishing, omit version to run that default, or keep it to run a specific completed version.