Developer APIRuns and results

Runs and results

Every managed run is a durable job on the server. This page covers what you send, what you get back, and the Python patterns for waiting, cancelling and running in parallel.

There are two kinds of run, and GET /runs lists both:

  • Release runs (kind: release) run a published Hub lab directly from its reference, as below. They use compute.
  • Managed runs (kind: managed) run an exact revision of a private lab after you approve a plan, as in Managed labs from code. They use the compute allowance or purchased compute of the account that owns the work.
  • Studio runs (kind: studio) were started in Biosimulant Studio, for example by Chat. You can read them here; cancel them in Studio.

Runs an assistant started for you over MCP or in Studio show up here too, and runs you start here show up in both.

Lifecycle

queued → running → completed
                 ↘ failed
                 ↘ timed_out
queued/running → cancelled

POST /runs returns 202 Accepted with a Location header. In Python, client.runs.create() hands you a Run (or AsyncRun) straight away, and client.run() creates the run and waits for it.

Lab references

Point a run at a lab with namespace/name or namespace/name@version. An unpinned reference is resolved once, before the run starts, and the exact version and package digest are saved with the run. Pin versions in production.

To see what you can run:

  • GET /capabilities lists the lab versions available to you.
  • GET /capabilities/{namespace}/{name} returns the latest version you can access, with its input and output schemas.
  • GET /compute-profiles lists CPU and GPU profiles, their limits and compute rates.

Private packages are only visible to their owner.

Select managed compute

Discover profiles with GET /compute-profiles, or MCP compute_profile_list. Pass a returned item id as compute_profile_id when preparing a managed run:

{
  "workspace_id": "<workspace_id>",
  "revision_id": "<revision_id>",
  "compute_profile_id": "<id returned by compute_profile_list>",
  "runtime_overrides": {"timeout_seconds": 300},
  "idempotency_key": "selected-profile-run-001"
}

The same field is supported on each experiment arm. Omit it for automatic selection; declare CPU/GPU support and minimum allocations in hardware.compute. An explicit profile must be active, configured for the platform’s provider, and compatible with the Lab. It is never silently replaced.

Discovery reports configuration and account access, not live capacity. The run plan binds the selected hardware, provider, timeout and estimated infrastructure cost to approval; configuration changes invalidate that plan. The platform selects the provider. Scientific compute uses the owning account’s allowance at the published rate.

Inputs and outputs

Input keys are the lab’s public names, not internal module aliases. Send a plain JSON value, or a value with a unit:

inputs={"initial_cells": 10}
inputs={"temperature": {"value": 37, "unit": "degC"}}

Unknown inputs, missing required values, wrong types and unsupported units return a 422 before any compute is reserved.

Results come back under the same public names:

result.outputs["population"]

Wait, time out and cancel

run.wait(timeout=...) polls with bounded exponential backoff. The timeout only applies to your code. When it runs out, RunTimeout carries the run handle and the server job keeps going.

from biosimulant import Client, RunTimeout
 
with Client() as client:
    run = client.runs.create(
        ref="demi/microbiology-hello-world-growth@1.0.0",
        metadata={"batch": "2026-07-19"},
    )
    try:
        result = run.wait(timeout=30)
    except RunTimeout as exc:
        save_run_id(exc.run.id)  # later: client.runs.retrieve(run_id)

Cancel with run.cancel(). Cancelling twice is safe, and a run that has already finished keeps its final state.

Async and parallel runs

AsyncClient waits without blocking the event loop, so you can start several runs at once:

import asyncio
from biosimulant import AsyncClient
 
async def main():
    async with AsyncClient() as client:
        results = await asyncio.gather(*[
            client.run(
                "demi/microbiology-hello-world-growth@1.0.0",
                inputs={"initial_cells": cells},
                timeout=300,
            )
            for cells in (10, 20, 40)
        ])
        return [result.outputs for result in results]
 
asyncio.run(main())

Your account’s compute concurrency limit still applies. Extra runs wait in the queue.

Progress and logs

GET /runs/{id}/events?after=<cursor> returns progress and log events. Save next_cursor and send it back as after on the next call. The SDK polls, so you don’t need WebSockets or SSE.

Results, artifacts and provenance

Once a run is completed, GET /runs/{id}/results returns the lab outputs, artifact descriptors and provenance. Download artifacts only through their run-scoped URLs, which check that the run belongs to you.

Provenance records the requested and resolved package refs, the package SHA-256 digest, the lab commit, runtime version, compute profile and execution provider. Solver settings, model resolutions and the random seed are kept too when the lab config includes them.

Managed labs from code

The Developer API runs the same operations as the MCP tools, for the API key’s owner only. Changes and compute go through a plan: prepare it, check it, then send back its exact ID and SHA-256.

API=https://api.biosimulant.com/v1
AUTH="Authorization: Bearer $BIOSIMULANT_API_KEY"
 
# 1. Fork a published release into a private lab.
curl -X POST "$API/workspaces" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"title": "Growth study", "source_ref": "demi/microbiology-hello-world-growth@1.0.0",
       "idempotency_key": "growth-study-001"}'
 
# 2. Prepare a run of its active revision and read the plan's findings and compute bounds.
curl -X POST "$API/run-plans" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"workspace_id": "<workspace_id>", "revision_id": "<revision_id>",
       "input_values": {"initial_cells": 10}, "idempotency_key": "growth-run-001"}'
 
# 3. Start it with the unmodified plan ID and digest.
curl -X POST "$API/runs" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"plan_id": "<plan_id>", "plan_sha256": "<plan_sha256>"}'

Change a lab the same way: POST /workspaces/{id}/change-plans, then POST /workspace-revisions with the plan. Compare several setups in one study with POST /experiment-plans and POST /experiments, and publish with POST /publication-plans and POST /publications. Replaying an executed plan returns the original result rather than a duplicate.

Read a managed run with GET /runs/{id}, its log with GET /runs/{id}/events, one result file with GET /runs/{id}/artifacts/{artifact_id}/metadata?include_content=true, and its Evidence Passport with GET /passports?run_id={id}. GET /runs/{id}/results maps declared outputs for release runs only.

Every response carries the resource’s fields at the top level, plus request_id, warnings, next_actions and links. Each next_actions entry names the MCP tool and gives the matching request under http.

Finding your work

ListFilters
GET /runsstatus, kind, workspace_id, experiment_id, lab_id
GET /experimentsstatus, workspace_id
GET /workspacesquery
GET /labsquery, visibility
GET /filesquery, format

Each list is newest first. Pass next_cursor back as cursor for the next page. GET /account reports your namespace, the key’s scopes, whether managed compute is on, and today’s fair-use usage.