> ## Documentation Index
> Fetch the complete documentation index at: https://docs.evolvingmachines.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Python SDK

> Start a job, wait for it, read the result, and download it from Python.

```bash theme={null}
pip install evolvingmachines-evolve
```

The hosted surface is a set of clients, one per noun. Each one reads `EVOLVE_API_KEY` from the environment, or takes a `HostedClientConfig(api_key=..., base_url=...)`. Create a key on the dashboard's [API keys page](https://dashboard.evolvingmachines.ai/api-keys).

Every call is a coroutine, and every client is an async context manager with `close()`. A request never follows a redirect: the client refuses every 3xx and raises it as an error, so the key only reaches the host you configured.

```python theme={null}
import asyncio
from evolve import jobs, trials, datasets

evals = jobs()
```

## Start a job

```python theme={null}
job = await evals.start(
    datasets=[{"name": "terminal-bench-4", "version": "4.0", "n_tasks": 5}],
    agents=[{"name": "codex", "model_name": "gpt-6-astra"}],
    n_attempts=1,
    n_concurrent_trials=4,
    max_trial_spend_usd=25,
)

print(job.id, job.status)   # "QUEUED"
```

`datasets` is a list of selectors, as dicts or `DatasetSelector` dataclasses. A bare `name` resolves to the active version; `task_names`, `exclude_task_names` and `n_tasks` narrow the task set. Every arm in `agents`, a dict or an `AgentArm`, names a `model_name`, and `version`, `reasoning_effort`, `kwargs`, `preset` and `skills` are optional.

`sandbox_provider` picks `"e2b"`, `"daytona"` or `"modal"`. The retry policy is a `JobRetryConfigInput` dict and comes back resolved as `JobRetryConfig`. The fields are the same ones a `-c` config file takes on the CLI.

Results are dataclasses with the wire's `snake_case` names and `Literal` vocabularies for every closed set. Four keys the wire spells in camelCase are mapped: `next_cursor`, `has_more`, `by_status` and `task_matrix`. A refused request raises `EvolveAPIError`; see [errors](/sdk-reference/errors).

## Wait for it

`watch()` returns a handle you either await, for the final job, or iterate, for each event. Pick one form per call.

```python theme={null}
final = await evals.watch(job.id)
print(final.status, final.trials.by_status, final.stats.get("cost_usd"))
```

```python theme={null}
async for event in evals.watch(job.id):
    if event.type == "trial.settled":
        print(event.data["task_name"], event.data["status"], event.data.get("reward"))
```

The stream replays from the beginning, so attaching late loses nothing, and it resumes on its own after a disconnect. `event.data` is a plain dict; branch on `event.type` and read it by key.

## Read the result

```python theme={null}
detail = await evals.get(job.id)

async for trial in evals.trials(job.id):
    print(trial.task_name, trial.agent_info.name, trial.status, trial.reward)

failures = await evals.trials(
    job.id,
    status=["INFRASTRUCTURE_ERROR", "SCORING_ERROR"],
)
```

Every list on this surface works the same way: await it for one page, iterate it to walk every page. `pass_at_k(job)` reads a finished job's pass\@k out of its stats as sorted numbers.

A trial id is global, so `trials()` reaches one without the job.

```python theme={null}
t = trials()

trial = await t.get(trial_id)
print(trial.reward, trial.exception_info and trial.exception_info.exception_type)

verifier_log = await t.artifact(trial_id, "verifier")   # str | None
home = await t.artifact(trial_id, "agent-home")         # dict[str, str] | None

async for event in t.trace_events(trial_id):
    print(event.seq, event.type)
```

`artifact()` takes the same names as `evolve trial download --stream`. None means the trial never stored that artifact; it is a normal answer, not an error.

## Download

```python theme={null}
path = await evals.download(
    job.id,
    to="./results",
)   # the saved file's path
data = await evals.download(job.id)                    # bytes
```

The archive is the standard job directory, the same tree `evolve job download` unpacks.

## Analyze

```python theme={null}
await evals.analyze(
    job.id,
    failing=True,
    n_trials=20,
)
analyzed = await evals.watch_analysis(job.id)
```

`analyze()` enqueues one analysis per trial and returns at once. `watch_analysis()` follows the batch of analyses to its end.

## The catalog

```python theme={null}
catalog = datasets()

async for dataset in catalog.list():
    print(dataset.name, dataset.active_version and dataset.active_version.version)

version = await catalog.get("terminal-bench-4@4.0")
```

## Everything else

`analyses()`, `checks()`, `skills()`, `agents()`, `auth()` and `orgs()` are built the same way. `meta()` fetches the platform's capability document, the live list of harnesses, models and limits, and needs no key. `hosted()` builds every client from one configuration.

```python theme={null}
from evolve import hosted

evolve = hosted()
doc = await evolve.meta()
job = await evolve.jobs.start(...)
```

Run any of the snippets above inside an `async def main()` and start it with `asyncio.run(main())`.
