> ## 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](/build/parameterize/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](/api-client#default-project) configured on `Ionworks(...)` or the `IONWORKS_PROJECT_ID` environment variable.

### Data references in pipelines

Use these prefixes to reference data sources in pipeline configs:

| Prefix    | Example               | Description                             |
| --------- | --------------------- | --------------------------------------- |
| `db:`     | `"db:measurement-id"` | Reference an uploaded measurement by ID |
| `file:`   | `"file:data.csv"`     | Load a local CSV file                   |
| `folder:` | `"folder:data_dir/"`  | Load from a local directory             |

<Tip>
  For inline DataFrames in pipeline configs, there is a 1,000-row limit.
  Upload larger datasets as measurements first, then reference them with
  `db:measurement-id`.
</Tip>

<Note>
  A `db:` measurement that is still being recorded grows as rows are appended,
  so an unpinned config reads more data on a later run than it did on the
  first. Add a `time_range` alongside `data` to pin the run to a fixed window
  of elapsed seconds:

  ```python theme={null}
  {"data": "db:measurement-id", "time_range": {"start": 0, "end": 3600}}
  ```

  See [pinning a measurement to a time window](/data/reading#pinning-a-measurement-to-a-time-window).
</Note>

<Note>
  The `folder:` scheme expects a directory containing `time_series` and
  `steps` files. Both `.parquet` and `.csv` are supported, and parquet is
  preferred when both are present. For example, a folder with
  `time_series.parquet` and `steps.parquet` (or `.csv`) loads correctly.
</Note>

## PyBaMM model support

You can hand an objective a PyBaMM model object directly instead of naming a
built-in model. It is serialized for you when the config is built — there is no
registration step and no string name to look up.

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

objective = iws.objectives.CurrentDriven(
    data_input="path/to/discharge.csv",
    options={"model": pybamm.lithium_ion.DFN()},
)
```

Model options travel with the object, so a configured model arrives configured:

```python theme={null}
model = pybamm.lithium_ion.DFN(options={"thermal": "lumped"})

objective = iws.objectives.CurrentDriven(
    data_input="path/to/discharge.csv",
    options={"model": model},
)
```

## 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

Each element returns a typed result object. Ask the pipeline for an element by
the name you gave it, index the fitted parameters, and plot:

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

print(fit.parameter_values["Positive particle diffusivity [m2.s-1]"])

fit.plot_fit_results()   # measured vs fitted, one figure per objective
fit.plot_trace()         # optimizer cost + parameter convergence
```

The class you get back reflects what the element did — `OptimizationResult`,
`PosteriorResult`, `ValidationResult` and so on, all sharing a `BaseResults`
base — so `parameter_values`, `to_config()` and the plots work the same way on
any of them. `result.results` returns every element at once, keyed by name.

Plotting needs the optional extra: `pip install ionworks-schema[plot]`.

<Note>
  The overlay and the optimizer trace are fetched on first access, not when the
  result is built, so reading `parameter_values` costs nothing extra.
</Note>

The raw payloads are still there if you want them — `result.result` for the
pipeline's final parameter values and `result.element_results` for per-element
dicts, keyed exactly as you passed them to `iws.Pipeline(elements=...)`.

### Element metadata

Elements write large fields (a validation's full per-point comparison, for
example) to a metadata blob rather than into `element_results`. The result object
already reads what it needs from there; fetch the blob yourself only for fields
it does not expose:

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

### Data-fit parameter trace

`fit.plot_trace()` draws the optimizer's per-iteration progress, and `fit.trace`
gives the raw records if you would rather plot them yourself — see
[Inspecting the parameter trace](/optimize/api#inspecting-the-parameter-trace)
for the schema. Neither needs the element's job ID; the result object resolves
that for you.

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

`fit.plot_fit_results()` above covers the common case. Drop to
`client.job.get_plot_data` when you want the raw traces — to feed your own
plotting stack, or to refetch detail as a user zooms. It 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.

The endpoint is keyed by job ID, which the elements listing carries:

```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")

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

Traces are decimated server-side to at most `max_points` points per series
(default `2000`, range `100`–`80000`). 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`–`80000`). 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)

# Filter, order, and paginate server-side
active = client.pipeline.list(status="in.(pending,running)")
matches = client.pipeline.list(name="ilike.%diffevo%")
recent = client.pipeline.list(order_by="created_at", order="desc", offset=25)
```

`list()` accepts `id`, `name`, `description`, `status`, `created_by_email`,
`created_at` and `updated_at` (plus their `_gt` / `_lt` range variants),
`order_by`, `order`, `limit`, and `offset`.

The result is a `PaginatedList`: it behaves like a list — iterate it, index it,
call `len()` on it — and also carries `.items`, `.count`, and `.total`, where
`.total` counts every matching pipeline rather than the rows in this page.

## 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`, `ArrayDataFit`, 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`, `ArrayDataFit`, 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"])
```

Like `client.pipeline.wait_for_completion`, this also accepts `poll_interval` (seconds between polls, default `2`), `verbose` (print status updates, default `True`), and `raise_on_failure` (default `True`; pass `False` to get the failed response back instead of raising) keyword arguments, and returns once the run reaches a terminal status — `completed`, `failed`, or `canceled`.

`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.

### Listing, filtering, and sorting

`list` returns a `PaginatedList`, the same type `client.pipeline.list()`
returns: it behaves like a list and also carries `items`, `count`, and
`total`.
String filters accept either an exact value or an operator-prefixed
expression such as `ilike.%foo%` (case-insensitive contains) or
`in.(completed,failed)` (match any of a set).

```python theme={null}
# Newest 25 simple pipelines in the default project
page = client.simple_pipeline.list(limit=25)

# Only currently active runs
active = client.simple_pipeline.list(status="in.(pending,running)")

# Name contains "diffevo" (case-insensitive)
matches = client.simple_pipeline.list(name="ilike.%diffevo%")

# Created in the last week, oldest first
recent = client.simple_pipeline.list(
    created_at_gt="2026-05-08",
    created_at_lt="2026-05-15",
    order_by="created_at",
    order="asc",
)
```

### Execution options

<Note>
  `data_fit` elements in simple pipelines are evaluated in parallel using
  the same distributed worker pool as regular pipelines. No extra
  configuration is required — set `optimizer.population_size` as usual
  and the server fans the population evaluations out across workers.
</Note>

Pass an `options` dict to `create` to control runtime execution behavior
for the submitted pipeline. Options are submission metadata — they affect
how the server runs the job but are not stored as part of the pipeline
config.

| Option                  | Type           | Default                                                   | Effect                                                                                                                                                                                   |
| ----------------------- | -------------- | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `live_progress_updates` | `bool \| None` | `None` (worker picks a sensible default for the job type) | When `True`, the worker writes checkpoint progress to the database during execution so you can poll intermediate progress. When `False`, checkpoints are skipped for better performance. |

```python theme={null}
sp = client.simple_pipeline.create(
    config,
    name="NMC622 diffusivity fit",
    options={"live_progress_updates": False},  # skip checkpointing for speed
)
```

You can also embed `options` (along with `project_id`, `name`, or
`description`) directly in the config dict — `create` lifts them out of
the config before submission. Arguments passed explicitly to `create`
take precedence over values found in the config.

```python theme={null}
config = {
    "options": {"live_progress_updates": True},
    "elements": { ... },
}

sp = client.simple_pipeline.create(config)
```

### Updating, cancelling, and deleting

```python theme={null}
# Rename or add a description (PATCH — at least one field is required)
client.simple_pipeline.update(sp.id, name="Renamed", description="notes")

# Cancel a running pipeline
client.simple_pipeline.cancel(sp.id)

# Permanently delete the pipeline, its job, and stored config
client.simple_pipeline.delete(sp.id)
```

### Handling errors

```python theme={null}
from ionworks.errors import IonworksError

try:
    result = client.simple_pipeline.wait_for_completion(sp.id)
except TimeoutError:
    # Still running after the timeout — fetch the latest status to decide
    current = client.simple_pipeline.get(sp.id)
    print(f"Still {current.status}")
except IonworksError as e:
    # Pipeline ended in "failed" — the message includes the server error
    print(e)
```

### 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"])
```

`result.result_object` gives the same run as a typed result object, with the
`plot_fit_results()` method and the lazy `overlay` / `series` channels described
under [Pipeline results](#retrieving-results). A validation carries no optimizer
trace, so `plot_trace()` raises on this one:

```python theme={null}
validation = result.result_object
validation.plot_fit_results()
```

## 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("fit").parameter_values)
```

<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>
