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

# Python API

> Submit, monitor, list, and retrieve results for pipeline jobs with the ionworks-api Python client.

The [`ionworks-api`](https://github.com/ionworks/ionworks-api) Python package exposes `client.pipeline` for running and managing [pipelines](/pipelines/overview). For installation and authentication, see the [Python API client](/api-client) page.

## Submitting a pipeline

```python theme={null}
import ionworks_schema as iws
from ionworks import Ionworks

# Reads IONWORKS_API_KEY and IONWORKS_PROJECT_ID from the environment.
client = Ionworks()

pipeline = iws.Pipeline(
    {
        "known": iws.direct_entries.DirectEntry(
            parameters={"Ambient temperature [K]": 298.15},
        ),
        "Q_pos": iws.calculations.ElectrodeCapacity(electrode="positive"),
    },
    name="Capacity pipeline",
)

submission = client.pipeline.create(pipeline)
print(f"Pipeline ID: {submission.id}")
print(f"Status: {submission.status}")
```

`client.pipeline.create()` accepts either an `iws.Pipeline` schema instance or the dict returned by `.to_config()`. Schema instances are validated locally before submission, so shape errors surface immediately.

### Serializing a pipeline to JSON

Use `.to_config()` when you want to inspect, cache, or transport the pipeline payload as JSON — for example to review it before submission, commit it to version control, or hand it off to another process.

```python theme={null}
import json

config = pipeline.to_config()

# Inspect or save
with open("pipeline_config.json", "w") as f:
    json.dump(config, f, indent=2)

# Submit later, or from another process
submission = client.pipeline.create(config)
```

<Warning>
  `.to_config()` is the only supported serializer. It emits the discriminators pipeline elements and objectives need (top-level elements are keyed on `element_type`, and nested schemas carry their own `type`), and emits each field under its wire name (for example, `data_input` → `data`).

  Do **not** use Pydantic's `model_dump()` to build API payloads. `model_dump()` drops these discriminators and emits Python attribute names instead of wire names, so it produces a dict the API may reject.
</Warning>

### Overriding submission metadata

`create()` accepts optional `project_id`, `name`, `description`, and `options` kwargs that override any values carried on the schema:

```python theme={null}
submission = client.pipeline.create(
    pipeline,
    project_id="your-project-id",
    name="Custom run name",
    options={"live_progress_updates": True},
)
```

When `project_id` is omitted, the client falls back to the default configured on `Ionworks(...)` or the `IONWORKS_PROJECT_ID` environment variable.

## Waiting for completion

```python theme={null}
submission = client.pipeline.wait_for_completion(
    submission.id,
    timeout=600,        # seconds (default: 600)
    poll_interval=2,    # seconds between polls (default: 2)
    verbose=True,       # print status updates (default: True)
)
```

Pass `raise_on_failure=False` to get the failed submission response back instead of raising when the pipeline errors out.

## Retrieving results

```python theme={null}
result = client.pipeline.result(submission.id)

# Final parameter values produced by the pipeline
print(result.result)

# Per-element output keyed by the element name used in the Pipeline
print(result.element_results["Q_pos"])
```

`result.element_results` mirrors the keys you passed to `iws.Pipeline(elements=...)`.

### Element metadata

Some elements (notably `Validation`) write extra metadata that isn't included in `element_results`. Fetch it with:

```python theme={null}
metadata = client.pipeline.get_element_metadata(submission.id, "validate")
```

### Data-fit parameter trace

Data-fit elements log the optimizer's per-iteration progress to the element's
job metadata. Pull it down with `client.job.get_parameter_trace` using the
element's job ID — see [Inspecting the parameter trace](/optimize/api#inspecting-the-parameter-trace)
for the full schema.

The element's `job_id` lives in the pipeline's elements list rather than in
`element_results`, so fetch the list and pick out the data-fit element by name:

```python theme={null}
elements = client.get(f"/pipelines/{submission.id}/elements")
fit_job_id = next(e["job_id"] for e in elements if e["name"] == "fit")

trace = client.job.get_parameter_trace(fit_job_id)
```

### Data-fit model-vs-data plot data

`client.job.get_plot_data` returns the model-vs-data traces for a data-fit job
— the same overlay Studio renders on the fit's results page. A data-fit
re-runs a validation on its best-fit parameters and stores the overlay in its
own metadata, so you can fetch it directly from the fit's job ID without
adding a separate `Validation` element or parsing the raw metadata blob.

```python theme={null}
plot = client.job.get_plot_data(
    fit_job_id,
    objective_name="1C",   # key used in the DataFit's objectives mapping
)
```

Use this when you want to reproduce the fit overlay in a notebook or report,
or feed it into your own plotting pipeline.

Traces are decimated server-side to at most `max_points` points per series
(default `2000`, range `100`–`10000`). For semantic zoom — refetching more
detail as a user zooms in — pass `x_min` and `x_max` set to the current
viewport:

```python theme={null}
zoomed = client.job.get_plot_data(
    fit_job_id,
    objective_name="1C",
    x_min=1200.0,
    x_max=1800.0,
    max_points=5000,
)
```

| Parameter        | Description                                                                             |
| ---------------- | --------------------------------------------------------------------------------------- |
| `job_id`         | The data-fit job whose overlay to fetch.                                                |
| `objective_name` | Key in the `DataFit`'s `objectives` mapping (for example, `"1C"` in the example above). |
| `plot_index`     | Index into the objective's list of plots when it defines several. Defaults to `0`.      |
| `max_points`     | Maximum points returned per trace (`100`–`10000`). Defaults to `2000`.                  |
| `x_min`, `x_max` | Inclusive x-range bounds. Omit for the full range.                                      |

## Listing pipelines

```python theme={null}
# Defaults to the project on Ionworks(...) or IONWORKS_PROJECT_ID
pipelines = client.pipeline.list()

# Override the project for a single call
pipelines = client.pipeline.list(project_id="other-project-id")

# Limit the number of results
pipelines = client.pipeline.list(limit=10)
```

## Getting a single submission

```python theme={null}
submission = client.pipeline.get(submission.id)
print(submission.status)  # "pending", "running", "completed", or "failed"
```

## SimplePipeline

A [`SimplePipeline`](/guide/pipelines/simple-pipelines) is a lightweight alternative to `Pipeline` for workflows with **at most one expensive element** — a single `DataFit` or `Validation`. It runs fire-and-forget and returns a flat result containing `parameter_values`, `cost`, and (for validation) `summary_stats`. Submit and poll it through `client.simple_pipeline`.

### Building the config

`SimplePipeline` inherits everything from `Pipeline` and adds client-side validation that rejects configs with more than one expensive element:

```python theme={null}
import ionworks_schema as iws

objective = iws.objectives.CurrentDriven(
    data_input="file:path/to/discharge.csv", options={"model": "SPM"}
)
pipeline = iws.SimplePipeline(
    elements={
        "initial_params": iws.direct_entries.DirectEntry(
            parameters={"Negative particle diffusivity [m2.s-1]": 2e-14},
        ),
        "fit": iws.DataFit(
            objectives={"cycle": objective},
            parameters={
                "Negative particle diffusivity [m2.s-1]": iws.Parameter(
                    "Negative particle diffusivity [m2.s-1]",
                    bounds=(1e-14, 1e-13),
                    initial_value=2e-14,
                )
            },
            cost=iws.costs.RMSE(),
            optimizer=iws.parameter_estimators.ScipyDifferentialEvolution(
                maxiter=10
            ),
        ),
    },
    name="My Fit",
)

config = pipeline.to_config()
```

<Note>
  If you pass more than one `DataFit` or `Validation` element, `SimplePipeline` raises a `ValueError` immediately — no need to wait for a server-side rejection.
</Note>

### Submitting and polling

```python theme={null}
from ionworks import Ionworks

client = Ionworks()

# Submit — pass the schema instance directly (a dict from .to_config() also works)
sp = client.simple_pipeline.create(pipeline, name=pipeline.name)
# sp.status == "pending"

# Wait for completion (polls automatically)
result = client.simple_pipeline.wait_for_completion(sp.id, timeout=600)

# Read results
print(result.result["parameter_values"])
# {"Negative particle diffusivity [m2.s-1]": 5.3e-14}
print(result.result["cost"])
```

`client.simple_pipeline.create()` mirrors `client.pipeline.create()` — it accepts either an `iws.SimplePipeline` schema instance or the dict returned by `.to_config()`. Prefer the schema instance: you don't need to call `.to_config()` yourself, and shape errors surface locally when you build the schema object. A raw dict is forwarded as-is, so a malformed dict is only rejected server-side as an HTTP 422.

### Validation pipelines

`SimplePipeline` also supports a single `Validation` element. The result includes `summary_stats` alongside `parameter_values`.

```python theme={null}
objective = iws.objectives.CurrentDriven(
    data_input="file:path/to/cycle.csv", options={"model": "SPM"}
)
validation_pipeline = iws.SimplePipeline(
    elements={
        "validate": iws.Validation(
            objectives={"cycle": objective},
        ),
    },
    name="Validate fitted model",
)

sp = client.simple_pipeline.create(
    validation_pipeline, name=validation_pipeline.name
)
result = client.simple_pipeline.wait_for_completion(sp.id, timeout=600)
print(result.result["summary_stats"])
```

## End-to-end example

```python theme={null}
import pybamm
import ionworks_schema as iws
from ionworks import Ionworks

client = Ionworks()

pipeline = iws.Pipeline(
    {
        "known": iws.direct_entries.DirectEntry(
            parameters={"Ambient temperature [K]": 298.15},
        ),
        "fit": iws.DataFit(
            objectives={
                "1C": iws.objectives.CurrentDriven(
                    data_input="file:examples/data/chen_synthetic_1C/time_series.csv",
                    options={"model": pybamm.lithium_ion.SPMe()},
                ),
            },
            parameters={
                "Negative particle diffusivity [m2.s-1]": iws.Parameter(
                    "Negative particle diffusivity [m2.s-1]",
                    initial_value=2e-14,
                    bounds=(1e-14, 1e-13),
                ),
            },
            cost=iws.costs.RMSE(),
            optimizer=iws.optimizers.DifferentialEvolution(),
        ),
    },
    name="SPMe diffusivity fit",
)

submission = client.pipeline.create(pipeline)
client.pipeline.wait_for_completion(submission.id)

result = client.pipeline.result(submission.id)
print(result.element_results["fit"])
```

<Note>
  For more end-to-end examples (entry-only, calculation-only, datafit, validation), see [`packages/ionworks-api/examples/pipeline/`](https://github.com/ionworks/ionworks-api/tree/main/examples/pipeline) in the SDK repo.
</Note>
