AutoSynth Python SDK
Typed Python client and CLI for the AutoSynth experiment-check API. The core flow: a chemist registers an observation from the lab against an approved plan, submits a perception check, and gets back the status and a structured result. The SDK also covers review, replay, lineage, datasets, and evaluation.
- Sync and async clients (
AutoSynth,AsyncAutoSynth) with pydantic models for every response. - Retries with exponential backoff on 429/5xx and network errors, honoring
Retry-After. - Writes are only resent when that is safe: artifacts are content-addressed, and check submissions carry an
auto-generated
Idempotency-Key. runs.wait()polls with backoff. Failed runs come back as data with an error code; they don't raise.- An
autosynthcommand line, plus reproducible synthetic vial images so you can try everything without a camera.
Install
pip install autosynth-sdk # or: uv add autosynth-sdk
Requires Python 3.10+. The distribution is autosynth-sdk; the import name is autosynth.
Quickstart
from autosynth import AutoSynth, synthetic
with AutoSynth() as client: # AUTOSYNTH_API_KEY, AUTOSYNTH_BASE_URL from the environment
plan = client.plans.list(status="approved")[0].plan_ref
photo = synthetic.vial(plan, seed=7, scenario="positive").png # or open("vial.png", "rb").read()
run = client.checks.run(photo, plan) # register -> submit -> wait
print(run.status, run.outcome, run.result.confidence) # succeeded positive 1.0
The same flow, one step at a time:
artifact = client.artifacts.register("vial.png", "PLN-0001", metadata={"camera": "bay2-cam1"})
run = client.checks.submit("PLN-0001", artifact.artifact_id) # 201, usually status "queued"
run = client.runs.wait(run, timeout=60) # polls until succeeded | failed
if run.succeeded:
print(run.result.outcome, run.result.confidence, run.result.model, run.result.findings)
print(run.provenance.input_sha256, run.provenance.config_hash, run.provenance.code.git_sha)
else:
print(run.error.code, run.error.message) # e.g. image_quality: observation too blurry to assess
Authentication
Create an API key on the Developers page of the staff app at https://autosynth.bio, then export it:
export AUTOSYNTH_API_KEY=... # sent as Authorization: Bearer <key>
You can also pass the key directly: AutoSynth(api_key=...). Each key is bound to one project, and every id you
see (plans, artifacts, runs) is scoped to that project. Ids from other projects look the same as missing ones
and return 404. Keys act in staff mode. The customer-portal endpoints (/portal/*) need a customer's browser
session, so the SDK doesn't wrap them.
Hosted vs local
| Base URL | Auth | |
|---|---|---|
| Hosted (default) | https://api.autosynth.bio |
API key required |
Local docker compose up (monorepo) |
http://localhost:8000 |
none in dev mode; a key from WARP_API_KEYS if set |
export AUTOSYNTH_BASE_URL=http://localhost:8000 # or AutoSynth(base_url="http://localhost:8000")
Results and failures
A check ends with status set to succeeded or failed. A succeeded run has result.outcome of positive,
negative or uncertain, plus confidence, findings (for example crystals with bright_pixels, and
sharpness), the model name and version, config_hash, and latency_ms. Failed runs are persisted with
error.code:
error.code |
Meaning |
|---|---|
image_quality |
frame too blurry to assess (failed the sharpness gate) |
corrupt_input |
image can't be decoded (for example, a truncated upload) |
input_missing / input_integrity |
evidence is gone, or its bytes no longer match the sha256 |
timeout |
the model missed the 500 ms online deadline |
plan_not_approved / unknown_model |
the plan is draft or revoked, or the model name or version is unknown |
overloaded, inference_unavailable, model_error |
the inference service stayed unavailable after the server's retries |
storage_unavailable, infrastructure_error, orphaned, internal |
platform failures; no run is left running forever |
Registering by uri= (a file already in the deployment's store) copies the bytes into content-addressed storage
right away. If the source is missing, artifacts.register raises InvalidRequestError with code input_missing,
so the problem shows up before any check is submitted.
run.outcome is the effective outcome. If a reviewer annotated the run, it is the reviewer's label;
otherwise it is the model's. The model's own result stays in run.result.
Errors, retries, timeouts
from autosynth import AutoSynth, ConflictError, NotFoundError, RateLimitError, APIConnectionError
client = AutoSynth(timeout=30, max_retries=3) # timeout: seconds or an httpx2.Timeout
| Exception | When |
|---|---|
AutoSynthError |
base class for everything below |
APIStatusError (.status, .code, .message) |
any 4xx/5xx; .code is the API's machine-readable code |
BadRequestError 400, AuthenticationError 401, PermissionDeniedError 403, NotFoundError 404 |
|
ConflictError 409 |
idempotency_conflict, invalid_transition, already released |
InvalidRequestError 422 |
validation failed (for example invalid_metadata) |
RateLimitError 429, InternalServerError 5xx |
raised after the automatic retries run out |
APIConnectionError, APITimeoutError |
no HTTP response (DNS, refused connection, TLS error, timeout) |
WaitTimeoutError (also a TimeoutError) |
runs.wait() gave up; .run holds the last state seen |
Retry behaviour:
- Reads, artifact registration, dataset snapshots, and submits/replays (which carry an
Idempotency-Key) are retried on connection errors, timeouts, 408, 429 and 5xx. - Append-only or once-only writes (annotations, releases, training runs, request transitions) are retried only when the server provably did not act: connection refused, 429, or 503.
- Backoff is exponential with full jitter (0.5 s, 1 s, 2 s, ... capped at 8 s).
Retry-AfterandRetry-After-Mstake precedence, capped at 60 s. - To make a submit safe across processes, pass your own
idempotency_key=. The same key with the same body returns the original run; the same key with a different body raisesConflictError.
Async
import asyncio
from autosynth import AsyncAutoSynth, synthetic
async def main():
async with AsyncAutoSynth() as client:
frames = [synthetic.vial("PLN-0003", seed) for seed in range(32)]
runs = await asyncio.gather(*(client.checks.run(f.png, "PLN-0003", priority="bulk") for f in frames))
print(sum(r.outcome == "positive" for r in runs), "of", len(runs), "crystallized")
asyncio.run(main())
AsyncAutoSynth has the same resources and methods as AutoSynth; every call is awaited, and runs.iter() is
an async iterator.
CLI
$ autosynth synth vial.png --scenario positive # write a synthetic observation (no API call)
$ autosynth check vial.png --plan PLN-0001 --wait
registered art_94e6989b2aff3242 (3249 bytes, sha256 a8fc26cb8bbf…)
submitted run_8b3c181d9c973e9c on PLN-0001 (online, queued)
run_8b3c181d9c973e9c succeeded positive conf 1.00 vialnet@1.3.0 72 ms
plan PLN-0001 · artifact art_94e6989b2aff3242 · input sha256 a8fc26cb8bbf… · online
findings: crystals 1.00 (bright_pixels 155) · sharpness 1.03
events: queued → running → succeeded
$ autosynth runs list --status failed --limit 10
$ autosynth runs get run_8b3c181d9c973e9c
$ autosynth runs annotate run_8b3c181d9c973e9c uncertain --comment "glare on the vial wall"
$ autosynth runs replay run_8b3c181d9c973e9c --wait
$ autosynth lineage run_8b3c181d9c973e9c
run_8b3c181d9c973e9c run/perception · positive
├─ ← input_of art_94e6989b2aff3242 artifact/observation
├─ → produced art_c0bc71b9b9fa8344 artifact/result
└─ → replay_of run_babceea49588cae6 run/perception · positive
$ autosynth plans
$ autosynth health
Every command accepts --json, --base-url and --api-key, and reads the same environment variables as the
client. Exit codes:
0: success.1: the check ran but failed (for exampleimage_quality), or the wait timed out.2: an API, network, or usage error.
python -m autosynth works too.
API reference
All methods exist on both AutoSynth and AsyncAutoSynth. Every method returns a pydantic model from
autosynth.models.
| Method | HTTP | Returns |
|---|---|---|
client.health() |
GET /healthz |
Health |
plans.list(status=None) |
GET /plans |
list[Plan] |
plans.approve(plan_ref) |
POST /plans/{ref}/approve |
dict |
artifacts.register(source, plan_ref, kind="observation", metadata=None, *, uri=None, parent_artifact_id=None) |
POST /artifacts |
Artifact |
artifacts.get(artifact_id) |
GET /artifacts/{id} |
Artifact (with runs, children, metadata history) |
artifacts.list(kind=, plan_ref=, tag=, meta=, since=, limit=) |
GET /artifacts |
list[Artifact] |
artifacts.content(artifact_id) |
GET /artifacts/{id}/content |
bytes |
artifacts.presign(artifact_id) |
GET /artifacts/{id}/content?presign=true |
PresignedURL |
artifacts.update_metadata(artifact_id, metadata, author=None) |
PATCH /artifacts/{id}/metadata |
Artifact |
checks.submit(plan_ref, artifact_id, *, model=None, priority="online", idempotency_key=None) |
POST /runs |
Run |
checks.run(source, plan_ref, *, metadata=, model=, priority=, wait=True, timeout=120) |
register + submit + wait | Run |
runs.get(run_id) |
GET /runs/{id} |
Run |
runs.list(plan_ref=, status=, limit=, cursor=) |
GET /runs |
RunPage |
runs.iter(plan_ref=, status=, page_size=100) |
GET /runs (all pages) |
iterator of Run |
runs.wait(run, *, timeout=120, poll_interval=0.2, max_poll_interval=2.0) |
polls GET /runs/{id} |
Run |
runs.annotate(run_id, label, *, comment="", reviewer=None, status="accepted") |
POST /runs/{id}/annotations |
AnnotationResult |
runs.annotations(run_id) |
GET /runs/{id}/annotations |
RunAnnotations |
runs.replay(run_id, *, model="pinned", priority="bulk", idempotency_key=None) |
POST /runs/{id}/replay |
Run |
runs.release(run_id) |
POST /runs/{id}/release |
Release |
runs.export(since=None) |
GET /export/runs.jsonl |
list[dict] |
lineage.get(node_id, *, direction="both", depth=3) |
GET /lineage/{id} |
LineageGraph |
datasets.create(name, *, plan_ref=, model=, status="succeeded", since=, until=, labeled_only=False) |
POST /datasets |
Artifact |
datasets.manifest(artifact_id) |
GET /artifacts/{id}/content |
DatasetManifest |
evaluation.get(*, model=None, since=None, reference="annotations") |
GET /evaluation |
list[ModelEvaluation] |
stats.weekly(*, weeks=8, as_of=None) |
GET /stats/weekly |
list[WeeklyStats] |
training_runs.list(kind=, limit=) / training_runs.create(kind, *, dataset_artifact_ids, config_hash, ...) |
GET / POST /training/runs |
TrainingRun |
requests.list(status=) / requests.plan(id, title=) / requests.reject(id, reason) |
staff request workflow | StaffRequest |
Notes:
sourceis a file path orbytes. Re-registering the same bytes returns the same artifact.- Models keep fields they don't recognise (
extra="allow"), so a newer server doesn't break an older SDK.
Synthetic observations
autosynth.synthetic.vial(plan_ref, seed, scenario) returns a reproducible 64×64 PNG plus capture metadata. It
is byte-identical to the frames the platform's robot mock produces. The hosted mock model answers each scenario
as follows:
| Scenario | Status | Result |
|---|---|---|
positive |
succeeded | positive |
negative |
succeeded | negative |
uncertain |
succeeded | uncertain (faint evidence after a short settle) |
blurry |
failed | image_quality |
corrupt |
failed | corrupt_input (truncated upload) |
None |
varies | sampled from the policy |
metadata["truth"] holds the later lab-confirmed outcome, which evaluation.get(reference="truth") scores
against.
Examples
examples/quickstart.py: the 10-line version.examples/chemist_demo.py: every success, uncertain, negative, and failure case; idempotent resubmits; reviewer correction; replay; lineage; an async bulk batch; datasets and evaluation. Exits non-zero if anything deviates from what's expected.notebooks/chemist_workflow.ipynb: the same workflow, executed against a local compose stack.
Compatibility
warp_sdk.WarpClient, the monorepo's pre-0.1 flat client (register_artifact, submit_check, get_run,
wait, ...), ships in the same wheel as a thin deprecated wrapper over AutoSynth (available as .client).
Development
cd sdk
uv venv && uv pip install -e . pytest anyio mypy
pytest # unit tests (httpx2.MockTransport); integration tests skip
AUTOSYNTH_INTEGRATION_URL=http://localhost:8000 AUTOSYNTH_INTEGRATION_KEY=... pytest -m integration
mypy --strict autosynth
uv build && uvx twine check dist/*
Releases go out by pushing a tag sdk-vX.Y.Z (the version must match pyproject.toml). The
sdk-publish GitHub workflow builds, tests, and publishes to PyPI through trusted publishing.
The terminal demo is recorded with docs/demo.sh.
License
MIT
Metadata
Release files for autosynth-sdk 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| autosynth_sdk-0.1.0.tar.gz | 68.9 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| autosynth_sdk-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 103.5 kB
Release files / autosynth_sdk-0.1.0.tar.gz
| Download URL | autosynth_sdk-0.1.0.tar.gz |
|---|---|
| Size | 68.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6f421ed15ccfe71888e38098740fcb3b7a08e36001f51a2726fe12f9d60b1553
|
|
BLAKE2b-256 checksum How to use checksums |
05b45339fc5151db7b909a224ef8f760195c2ecb6b469cb40c289c54c97bbe02
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency logRelease files / autosynth_sdk-0.1.0-py3-none-any.whl
| Download URL | autosynth_sdk-0.1.0-py3-none-any.whl |
|---|---|
| Size | 34.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
37ff2edb2425987e5109c34aad9e76799ebf1409139da0fde38ac5373c70f088
|
|
BLAKE2b-256 checksum How to use checksums |
e805e9fc44af040321326a857487b811134a5ca6ae23562e80e4ecb1b1b86d72
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency logSecurity for autosynth-sdk 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for autosynth_sdk-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.