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.

Download .md for agents

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.json inputs with "type": "sequence", "smiles" or "pdb", and the file is named after the input's name. Inputs that take a list of files ("list": true) don't; use includeMetadata for 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.
  • source is "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, labelled A, B, C in order; a SMILES gives one small_molecule entry; 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) and chains. chains can be empty when nothing could be read, for example a structure file with only ligands.
  • Can be null: moleculeId and groupId (molecules only), name (not filled in yet), file (pdb inputs only: {"path", "name", "fileId"}), and for each chain label, subtype, sequence (not for a small molecule) and smiles (small molecules only).
  • moleculeType is one of protein, nucleic_acid, small_molecule.
  • subtype is 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 is null for 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"]))

Full tool format details · Starter template

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)

View Python SDK reference

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.